Compare commits
142 Commits
Author | SHA1 | Date | |
---|---|---|---|
e372480249 | |||
b7766aeec2 | |||
5af1f09ee1 | |||
6646ceda76 | |||
0b05bb44a2 | |||
8cb2074d76 | |||
c408d62118 | |||
a90703e525 | |||
f74f9f69b2 | |||
a950587e84 | |||
a8dc366038 | |||
eaf653e3c0 | |||
5c391917d2 | |||
09b5aa7f93 | |||
66a8c17f1f | |||
adc31984a8 | |||
8114eef381 | |||
9190420c66 | |||
e5ba074936 | |||
e4f1efe26a | |||
498f4e7b79 | |||
d9a526e828 | |||
2f93b4980b | |||
ac0ca06d7f | |||
17320a19cf | |||
3faa251216 | |||
a9f140fabc | |||
b6b5a94bbd | |||
c05f0fe5cb | |||
eaf1423fb1 | |||
7feb13856d | |||
ca2ca9df3b | |||
4259915ff6 | |||
89e7a9b1ea | |||
8c25d23d09 | |||
f44811b9dc | |||
f65fd513f2 | |||
327c88a745 | |||
63aa8c2f44 | |||
afbfc55088 | |||
d13714c1ea | |||
92a6d506dc | |||
e381880675 | |||
7e185154ef | |||
27c25378b1 | |||
1159a6cc2e | |||
489bf13707 | |||
5d3faceb5a | |||
29d34a6b62 | |||
be80d74141 | |||
d37e86ce2c | |||
d8fc857d23 | |||
e0bec333ed | |||
46867ad25e | |||
924edf6f96 | |||
010c8eddde | |||
96602b1a9c | |||
d536499799 | |||
ad016c54b2 | |||
f8b0b38e9e | |||
912e549104 | |||
93bdb0ed7f | |||
87713bbe64 | |||
dce27059ff | |||
c919326480 | |||
1e124a90f3 | |||
d1fc9de4bd | |||
04db3f50ed | |||
4e347adf69 | |||
79de14c9aa | |||
7448e89965 | |||
967743715b | |||
d37913005b | |||
0986b97051 | |||
bbb04f7907 | |||
015f406ddf | |||
e1a97672b5 | |||
493b5945f9 | |||
52606aae8b | |||
2d6ca0f841 | |||
34900f929f | |||
5dbd05fdfb | |||
fe3cf9adb1 | |||
9bc8460cce | |||
4cbd2fd6eb | |||
6fef3fe37a | |||
72a20d1ed9 | |||
73ed80434a | |||
a78672927b | |||
4256eae442 | |||
76412b68f3 | |||
9153bb140a | |||
c9c15d10f9 | |||
e8c2277ef5 | |||
23720c5dc9 | |||
f881f07cba | |||
b072ee8dec | |||
acfbc0b66f | |||
c738a03934 | |||
84d62f3583 | |||
f9faa03b92 | |||
55f6c3c6cd | |||
a0c6753761 | |||
327125608a | |||
6c0045302b | |||
9c85bebe72 | |||
fb56cd551e | |||
e83d507e79 | |||
0c0e7fc87d | |||
faf49fbe1d | |||
2925b0ff79 | |||
acb18474c8 | |||
ed61490398 | |||
abb76273c9 | |||
2f15af4ff8 | |||
e29e8416d5 | |||
5d423210dd | |||
340c36fdf8 | |||
3085158b80 | |||
c2585f9bf4 | |||
d478137742 | |||
827bd182b1 | |||
f7500637fe | |||
7c884610b1 | |||
d3d219751e | |||
16d2e7d06f | |||
47b86ff9fa | |||
6e574b9154 | |||
485001403d | |||
694c5e7f7d | |||
d9ff26385a | |||
0cfb7c65bb | |||
3cbc2a0367 | |||
7733c90206 | |||
0ad6e5cb1c | |||
8a47cfd5f1 | |||
db4c0399fd | |||
b0f29b72cd | |||
98a1622ccc | |||
a6ac737590 | |||
bba719012f | |||
cdc0cea01c |
1
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
custom: ['https://nicoverbruggen.be/sponsor', 'https://paypal.me/nicoverbruggen']
|
@ -3,24 +3,57 @@
|
|||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 50;
|
objectVersion = 52;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* 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 */; };
|
C405A4D024B9B9140062FAFA /* InternetAccessPolicy.strings in Resources */ = {isa = PBXBuildFile; fileRef = C405A4CE24B9B9130062FAFA /* InternetAccessPolicy.strings */; };
|
||||||
C405A4D124B9B9140062FAFA /* InternetAccessPolicy.plist in Resources */ = {isa = PBXBuildFile; fileRef = C405A4CF24B9B9140062FAFA /* InternetAccessPolicy.plist */; };
|
C405A4D124B9B9140062FAFA /* InternetAccessPolicy.plist in Resources */ = {isa = PBXBuildFile; fileRef = C405A4CF24B9B9140062FAFA /* InternetAccessPolicy.plist */; };
|
||||||
C412E5FC25700D5300A1FB67 /* HomebrewPackage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */; };
|
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 */; };
|
C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */; };
|
||||||
C41C1B3B22B0098000E7CF16 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C41C1B3A22B0098000E7CF16 /* Assets.xcassets */; };
|
C41C1B3B22B0098000E7CF16 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C41C1B3A22B0098000E7CF16 /* Assets.xcassets */; };
|
||||||
C41C1B3E22B0098000E7CF16 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C41C1B3C22B0098000E7CF16 /* Main.storyboard */; };
|
C41C1B3E22B0098000E7CF16 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C41C1B3C22B0098000E7CF16 /* Main.storyboard */; };
|
||||||
C41C1B4722B009A400E7CF16 /* Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4622B009A400E7CF16 /* Shell.swift */; };
|
C41C1B4722B009A400E7CF16 /* Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4622B009A400E7CF16 /* Shell.swift */; };
|
||||||
C41C1B4922B00A9800E7CF16 /* MenuBarImageGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */; };
|
C41C1B4922B00A9800E7CF16 /* MenuBarImageGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */; };
|
||||||
C41C1B4B22B019FF00E7CF16 /* PhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4A22B019FF00E7CF16 /* PhpInstallation.swift */; };
|
C41C1B4B22B019FF00E7CF16 /* ActivePhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */; };
|
||||||
C41C1B4D22B0215A00E7CF16 /* Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4C22B0215A00E7CF16 /* Actions.swift */; };
|
C41C1B4D22B0215A00E7CF16 /* Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4C22B0215A00E7CF16 /* Actions.swift */; };
|
||||||
|
C41CA5ED2774F8EE00A2C80E /* SiteListVC+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41CA5EC2774F8EE00A2C80E /* SiteListVC+Actions.swift */; };
|
||||||
|
C41CA5EE2774F8EE00A2C80E /* SiteListVC+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41CA5EC2774F8EE00A2C80E /* SiteListVC+Actions.swift */; };
|
||||||
|
C41CD0292628D8EE0065BBED /* GlobalKeybindPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41CD0282628D8EE0065BBED /* GlobalKeybindPreference.swift */; };
|
||||||
|
C41E871A2763D42300161EE0 /* SiteListVC+ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41E87192763D42300161EE0 /* SiteListVC+ContextMenu.swift */; };
|
||||||
|
C41E871B2763D42300161EE0 /* SiteListVC+ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41E87192763D42300161EE0 /* SiteListVC+ContextMenu.swift */; };
|
||||||
C42295DD2358D02000E263B2 /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42295DC2358D02000E263B2 /* Command.swift */; };
|
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 */; };
|
C43A8A1A25D9CD1000591B77 /* Utility.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43A8A1925D9CD1000591B77 /* Utility.swift */; };
|
||||||
C43A8A2025D9D1D700591B77 /* brew.json in Resources */ = {isa = PBXBuildFile; fileRef = C43A8A1F25D9D1D700591B77 /* brew.json */; };
|
C43A8A2025D9D1D700591B77 /* brew.json in Resources */ = {isa = PBXBuildFile; fileRef = C43A8A1F25D9D1D700591B77 /* brew.json */; };
|
||||||
C43A8A2425D9D20D00591B77 /* BrewJsonParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43A8A2325D9D20D00591B77 /* BrewJsonParserTest.swift */; };
|
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 */; };
|
C46FA23F246C358E00944F05 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA23E246C358E00944F05 /* StringExtension.swift */; };
|
||||||
C473319F2470923A009A0597 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = C473319E2470923A009A0597 /* Localizable.strings */; };
|
C473319F2470923A009A0597 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = C473319E2470923A009A0597 /* Localizable.strings */; };
|
||||||
C47331A2247093B7009A0597 /* StatusMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47331A1247093B7009A0597 /* StatusMenu.swift */; };
|
C47331A2247093B7009A0597 /* StatusMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47331A1247093B7009A0597 /* StatusMenu.swift */; };
|
||||||
@ -28,15 +61,41 @@
|
|||||||
C476FF9822B0DD830098105B /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = C476FF9722B0DD830098105B /* Alert.swift */; };
|
C476FF9822B0DD830098105B /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = C476FF9722B0DD830098105B /* Alert.swift */; };
|
||||||
C4811D2422D70A4700B5F6B3 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4811D2322D70A4700B5F6B3 /* App.swift */; };
|
C4811D2422D70A4700B5F6B3 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4811D2322D70A4700B5F6B3 /* App.swift */; };
|
||||||
C4811D2A22D70F9A00B5F6B3 /* MainMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4811D2922D70F9A00B5F6B3 /* MainMenu.swift */; };
|
C4811D2A22D70F9A00B5F6B3 /* MainMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4811D2922D70F9A00B5F6B3 /* MainMenu.swift */; };
|
||||||
|
C481F79726164A78004FBCFF /* PrefsVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395826135DC100FB00FA /* PrefsVC.swift */; };
|
||||||
|
C481F79A26164A7C004FBCFF /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395E2613607600FB00FA /* Preferences.swift */; };
|
||||||
C48D0C9025CC7FD000CC7490 /* StatsView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C48D0C8F25CC7FD000CC7490 /* StatsView.xib */; };
|
C48D0C9025CC7FD000CC7490 /* StatsView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C48D0C8F25CC7FD000CC7490 /* StatsView.xib */; };
|
||||||
C48D0C9325CC804200CC7490 /* XibLoadable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D0C9225CC804200CC7490 /* XibLoadable.swift */; };
|
C48D0C9325CC804200CC7490 /* XibLoadable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D0C9225CC804200CC7490 /* XibLoadable.swift */; };
|
||||||
C48D0C9625CC80B100CC7490 /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D0C9525CC80B100CC7490 /* HeaderView.swift */; };
|
C48D0C9625CC80B100CC7490 /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D0C9525CC80B100CC7490 /* HeaderView.swift */; };
|
||||||
C48D0C9A25CC888B00CC7490 /* HeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C48D0C9925CC888B00CC7490 /* HeaderView.xib */; };
|
C48D0C9A25CC888B00CC7490 /* HeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C48D0C9925CC888B00CC7490 /* HeaderView.xib */; };
|
||||||
C48D0CA325CC992000CC7490 /* StatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D0CA225CC992000CC7490 /* StatsView.swift */; };
|
C48D0CA325CC992000CC7490 /* StatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D0CA225CC992000CC7490 /* StatsView.swift */; };
|
||||||
|
C4998F0626175E7200B2526E /* HotKey in Frameworks */ = {isa = PBXBuildFile; productRef = C4998F0526175E7200B2526E /* HotKey */; };
|
||||||
|
C4998F0A2617633900B2526E /* PrefsWC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4998F092617633900B2526E /* PrefsWC.swift */; };
|
||||||
|
C4998F0B2617633900B2526E /* PrefsWC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4998F092617633900B2526E /* PrefsWC.swift */; };
|
||||||
C49EAB46259FC305007F6C3B /* Paths.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EAB45259FC305007F6C3B /* Paths.swift */; };
|
C49EAB46259FC305007F6C3B /* Paths.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EAB45259FC305007F6C3B /* Paths.swift */; };
|
||||||
C4ACA38F25C754C100060C66 /* PhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4ACA38E25C754C100060C66 /* PhpExtension.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 */; };
|
C4D8016622B1584700C6DA1B /* Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D8016522B1584700C6DA1B /* Startup.swift */; };
|
||||||
C4EE188422D3386B00E126E5 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE188322D3386B00E126E5 /* Constants.swift */; };
|
C4EE188422D3386B00E126E5 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE188322D3386B00E126E5 /* Constants.swift */; };
|
||||||
|
C4F2E4372752F0870020E974 /* HomebrewDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4362752F0870020E974 /* HomebrewDiagnostics.swift */; };
|
||||||
|
C4F2E4382752F08D0020E974 /* HomebrewDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4362752F0870020E974 /* HomebrewDiagnostics.swift */; };
|
||||||
|
C4F2E43A2752F7D00020E974 /* PhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4392752F7D00020E974 /* PhpInstallation.swift */; };
|
||||||
|
C4F2E43B27530F750020E974 /* PhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4392752F7D00020E974 /* PhpInstallation.swift */; };
|
||||||
C4F7809625D7FBF8000DBC97 /* Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4622B009A400E7CF16 /* Shell.swift */; };
|
C4F7809625D7FBF8000DBC97 /* Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4622B009A400E7CF16 /* Shell.swift */; };
|
||||||
C4F7809C25D80344000DBC97 /* CommandTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F7809B25D80344000DBC97 /* CommandTest.swift */; };
|
C4F7809C25D80344000DBC97 /* CommandTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F7809B25D80344000DBC97 /* CommandTest.swift */; };
|
||||||
C4F7809F25D8037C000DBC97 /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42295DC2358D02000E263B2 /* Command.swift */; };
|
C4F7809F25D8037C000DBC97 /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42295DC2358D02000E263B2 /* Command.swift */; };
|
||||||
@ -58,10 +117,11 @@
|
|||||||
C4F780C925D80B75000DBC97 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA23E246C358E00944F05 /* StringExtension.swift */; };
|
C4F780C925D80B75000DBC97 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA23E246C358E00944F05 /* StringExtension.swift */; };
|
||||||
C4F780CA25D80B75000DBC97 /* HomebrewPackage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */; };
|
C4F780CA25D80B75000DBC97 /* HomebrewPackage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */; };
|
||||||
C4F780CB25D80B75000DBC97 /* StatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D0CA225CC992000CC7490 /* StatsView.swift */; };
|
C4F780CB25D80B75000DBC97 /* StatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D0CA225CC992000CC7490 /* StatsView.swift */; };
|
||||||
C4F780CC25D80B75000DBC97 /* PhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4A22B019FF00E7CF16 /* PhpInstallation.swift */; };
|
C4F780CC25D80B75000DBC97 /* ActivePhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */; };
|
||||||
C4F780CD25D80B75000DBC97 /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = C476FF9722B0DD830098105B /* Alert.swift */; };
|
C4F780CD25D80B75000DBC97 /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = C476FF9722B0DD830098105B /* Alert.swift */; };
|
||||||
C4F780CE25D80B75000DBC97 /* LocalNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = C474B00524C0E98C00066A22 /* LocalNotification.swift */; };
|
C4F780CE25D80B75000DBC97 /* LocalNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = C474B00524C0E98C00066A22 /* LocalNotification.swift */; };
|
||||||
C4F8C0A422D4F12C002EFE61 /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8C0A322D4F12C002EFE61 /* DateExtension.swift */; };
|
C4F8C0A422D4F12C002EFE61 /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8C0A322D4F12C002EFE61 /* DateExtension.swift */; };
|
||||||
|
C4FBFC532616485F00CDB8E1 /* PhpVersionDetectionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4FBFC512616485F00CDB8E1 /* PhpVersionDetectionTest.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@ -75,9 +135,18 @@
|
|||||||
/* End PBXContainerItemProxy section */
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
5420395826135DC100FB00FA /* PrefsVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsVC.swift; sourceTree = "<group>"; };
|
||||||
|
5420395E2613607600FB00FA /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
|
||||||
|
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>"; };
|
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>"; };
|
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>"; };
|
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; };
|
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>"; };
|
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>"; };
|
C41C1B3A22B0098000E7CF16 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
@ -86,12 +155,21 @@
|
|||||||
C41C1B4022B0098000E7CF16 /* phpmon.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = phpmon.entitlements; sourceTree = "<group>"; };
|
C41C1B4022B0098000E7CF16 /* phpmon.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = phpmon.entitlements; sourceTree = "<group>"; };
|
||||||
C41C1B4622B009A400E7CF16 /* Shell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shell.swift; sourceTree = "<group>"; };
|
C41C1B4622B009A400E7CF16 /* Shell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shell.swift; sourceTree = "<group>"; };
|
||||||
C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarImageGenerator.swift; sourceTree = "<group>"; };
|
C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarImageGenerator.swift; sourceTree = "<group>"; };
|
||||||
C41C1B4A22B019FF00E7CF16 /* PhpInstallation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpInstallation.swift; sourceTree = "<group>"; };
|
C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivePhpInstallation.swift; sourceTree = "<group>"; };
|
||||||
C41C1B4C22B0215A00E7CF16 /* Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Actions.swift; sourceTree = "<group>"; };
|
C41C1B4C22B0215A00E7CF16 /* Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Actions.swift; sourceTree = "<group>"; };
|
||||||
|
C41CA5EC2774F8EE00A2C80E /* SiteListVC+Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SiteListVC+Actions.swift"; sourceTree = "<group>"; };
|
||||||
|
C41CD0282628D8EE0065BBED /* GlobalKeybindPreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalKeybindPreference.swift; sourceTree = "<group>"; };
|
||||||
|
C41E87192763D42300161EE0 /* SiteListVC+ContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SiteListVC+ContextMenu.swift"; sourceTree = "<group>"; };
|
||||||
C42295DC2358D02000E263B2 /* Command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Command.swift; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
C47331A1247093B7009A0597 /* StatusMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMenu.swift; sourceTree = "<group>"; };
|
||||||
@ -104,12 +182,25 @@
|
|||||||
C48D0C9525CC80B100CC7490 /* HeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderView.swift; sourceTree = "<group>"; };
|
C48D0C9525CC80B100CC7490 /* HeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderView.swift; sourceTree = "<group>"; };
|
||||||
C48D0C9925CC888B00CC7490 /* HeaderView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = HeaderView.xib; sourceTree = "<group>"; };
|
C48D0C9925CC888B00CC7490 /* HeaderView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = HeaderView.xib; sourceTree = "<group>"; };
|
||||||
C48D0CA225CC992000CC7490 /* StatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsView.swift; sourceTree = "<group>"; };
|
C48D0CA225CC992000CC7490 /* StatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsView.swift; sourceTree = "<group>"; };
|
||||||
|
C4998F092617633900B2526E /* PrefsWC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsWC.swift; sourceTree = "<group>"; };
|
||||||
C49EAB45259FC305007F6C3B /* Paths.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Paths.swift; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
C4E713562570150F00007428 /* SECURITY.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = SECURITY.md; sourceTree = "<group>"; };
|
||||||
C4E713572570151400007428 /* docs */ = {isa = PBXFileReference; lastKnownFileType = folder; path = docs; sourceTree = "<group>"; };
|
C4E713572570151400007428 /* docs */ = {isa = PBXFileReference; lastKnownFileType = folder; path = docs; sourceTree = "<group>"; };
|
||||||
C4EE188322D3386B00E126E5 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
|
C4EE188322D3386B00E126E5 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
|
||||||
|
C4F2E4362752F0870020E974 /* HomebrewDiagnostics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewDiagnostics.swift; sourceTree = "<group>"; };
|
||||||
|
C4F2E4392752F7D00020E974 /* PhpInstallation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpInstallation.swift; sourceTree = "<group>"; };
|
||||||
C4F7807425D7F7E5000DBC97 /* RELEASE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = RELEASE.md; sourceTree = "<group>"; };
|
C4F7807425D7F7E5000DBC97 /* RELEASE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = RELEASE.md; sourceTree = "<group>"; };
|
||||||
C4F7807925D7F84B000DBC97 /* phpmon-tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "phpmon-tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
|
C4F7807925D7F84B000DBC97 /* phpmon-tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "phpmon-tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
C4F7807D25D7F84B000DBC97 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
C4F7807D25D7F84B000DBC97 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
@ -118,6 +209,7 @@
|
|||||||
C4F780AD25D80B37000DBC97 /* ExtensionParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionParserTest.swift; sourceTree = "<group>"; };
|
C4F780AD25D80B37000DBC97 /* ExtensionParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionParserTest.swift; sourceTree = "<group>"; };
|
||||||
C4F8C0A322D4F12C002EFE61 /* DateExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtension.swift; sourceTree = "<group>"; };
|
C4F8C0A322D4F12C002EFE61 /* DateExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtension.swift; sourceTree = "<group>"; };
|
||||||
C4F8C0A522D4FA41002EFE61 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
|
C4F8C0A522D4FA41002EFE61 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
|
||||||
|
C4FBFC512616485F00CDB8E1 /* PhpVersionDetectionTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhpVersionDetectionTest.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@ -125,6 +217,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
C4998F0626175E7200B2526E /* HotKey in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@ -138,6 +231,39 @@
|
|||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
|
5420395726135DB800FB00FA /* Preferences */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
C4998F092617633900B2526E /* PrefsWC.swift */,
|
||||||
|
5420395826135DC100FB00FA /* PrefsVC.swift */,
|
||||||
|
5420395E2613607600FB00FA /* Preferences.swift */,
|
||||||
|
C41CD0272628D8E20065BBED /* Keybinds */,
|
||||||
|
54FCFD28276C88C0004CE748 /* Views */,
|
||||||
|
);
|
||||||
|
path = Preferences;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
54B20EDF263AA22C00D3250E /* PHP */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */,
|
||||||
|
C4F2E4392752F7D00020E974 /* PhpInstallation.swift */,
|
||||||
|
C4ACA38E25C754C100060C66 /* PhpExtension.swift */,
|
||||||
|
);
|
||||||
|
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 */ = {
|
C405A4CD24B9B9070062FAFA /* IAP */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@ -172,10 +298,10 @@
|
|||||||
C41C1B3522B0097F00E7CF16 /* phpmon */ = {
|
C41C1B3522B0097F00E7CF16 /* phpmon */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */,
|
|
||||||
C4EE188322D3386B00E126E5 /* Constants.swift */,
|
C4EE188322D3386B00E126E5 /* Constants.swift */,
|
||||||
C41E181722CB61EB0072CF09 /* Domain */,
|
C41E181722CB61EB0072CF09 /* Domain */,
|
||||||
C41C1B3F22B0098000E7CF16 /* Info.plist */,
|
C41C1B3F22B0098000E7CF16 /* Info.plist */,
|
||||||
|
C4232EE42612526500158FC6 /* Credits.html */,
|
||||||
C41C1B4022B0098000E7CF16 /* phpmon.entitlements */,
|
C41C1B4022B0098000E7CF16 /* phpmon.entitlements */,
|
||||||
C41C1B3A22B0098000E7CF16 /* Assets.xcassets */,
|
C41C1B3A22B0098000E7CF16 /* Assets.xcassets */,
|
||||||
C473319E2470923A009A0597 /* Localizable.strings */,
|
C473319E2470923A009A0597 /* Localizable.strings */,
|
||||||
@ -184,18 +310,42 @@
|
|||||||
path = phpmon;
|
path = phpmon;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
C41CD0272628D8E20065BBED /* Keybinds */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
C41CD0282628D8EE0065BBED /* GlobalKeybindPreference.swift */,
|
||||||
|
);
|
||||||
|
path = Keybinds;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
C41E181722CB61EB0072CF09 /* Domain */ = {
|
C41E181722CB61EB0072CF09 /* Domain */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
C4F7808A25D7F918000DBC97 /* Terminal */,
|
C4AF9F6B275445D300D44ED0 /* Integrations */,
|
||||||
C4B13B1D25C4915000548C3A /* Core */,
|
C4B13B1D25C4915000548C3A /* Core */,
|
||||||
|
54B20EDF263AA22C00D3250E /* PHP */,
|
||||||
|
C4F7808A25D7F918000DBC97 /* Terminal */,
|
||||||
C47331A0247093AC009A0597 /* Menu */,
|
C47331A0247093AC009A0597 /* Menu */,
|
||||||
|
C464ADAA275A7A25003FCD53 /* SiteList */,
|
||||||
|
5420395726135DB800FB00FA /* Preferences */,
|
||||||
C4811D2822D70D9C00B5F6B3 /* Helpers */,
|
C4811D2822D70D9C00B5F6B3 /* Helpers */,
|
||||||
C4F8C0A222D4F100002EFE61 /* Extensions */,
|
C4F8C0A222D4F100002EFE61 /* Extensions */,
|
||||||
);
|
);
|
||||||
path = Domain;
|
path = Domain;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
C464ADAA275A7A25003FCD53 /* SiteList */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
C464ADAB275A7A3F003FCD53 /* SiteListWC.swift */,
|
||||||
|
C464ADAE275A7A69003FCD53 /* SiteListVC.swift */,
|
||||||
|
C41E87192763D42300161EE0 /* SiteListVC+ContextMenu.swift */,
|
||||||
|
C41CA5EC2774F8EE00A2C80E /* SiteListVC+Actions.swift */,
|
||||||
|
C464ADB1275A87CA003FCD53 /* SiteListCell.swift */,
|
||||||
|
);
|
||||||
|
path = SiteList;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
C47331A0247093AC009A0597 /* Menu */ = {
|
C47331A0247093AC009A0597 /* Menu */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@ -213,20 +363,55 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
C476FF9722B0DD830098105B /* Alert.swift */,
|
C476FF9722B0DD830098105B /* Alert.swift */,
|
||||||
C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */,
|
54B48B5E275F66AE006D90C5 /* Application.swift */,
|
||||||
|
C4188988275FE8CB001EF227 /* Filesystem.swift */,
|
||||||
C474B00524C0E98C00066A22 /* LocalNotification.swift */,
|
C474B00524C0E98C00066A22 /* LocalNotification.swift */,
|
||||||
|
C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */,
|
||||||
|
C4CCBA6B275C567B008C7055 /* PMWindowController.swift */,
|
||||||
|
54AB03252763858F00A29D5F /* Timer.swift */,
|
||||||
|
C4B5635D276AB09000F12CCB /* VersionExtractor.swift */,
|
||||||
);
|
);
|
||||||
path = Helpers;
|
path = Helpers;
|
||||||
sourceTree = "<group>";
|
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 = Homebrew;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
C4B13B1D25C4915000548C3A /* Core */ = {
|
C4B13B1D25C4915000548C3A /* Core */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
C41C1B3C22B0098000E7CF16 /* Main.storyboard */,
|
C41C1B3C22B0098000E7CF16 /* Main.storyboard */,
|
||||||
|
C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */,
|
||||||
|
C4B97B74275CF08C003F3378 /* AppDelegate+MenuOutlets.swift */,
|
||||||
|
C436039F275E67610028EFC6 /* AppDelegate+Notifications.swift */,
|
||||||
C4811D2322D70A4700B5F6B3 /* App.swift */,
|
C4811D2322D70A4700B5F6B3 /* App.swift */,
|
||||||
C41C1B4A22B019FF00E7CF16 /* PhpInstallation.swift */,
|
C4B97B77275CF1B5003F3378 /* App+ActivationPolicy.swift */,
|
||||||
C4ACA38E25C754C100060C66 /* PhpExtension.swift */,
|
C4B97B7A275CF20A003F3378 /* App+GlobalHotkey.swift */,
|
||||||
C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */,
|
C4D8016522B1584700C6DA1B /* Startup.swift */,
|
||||||
|
C41C1B4C22B0215A00E7CF16 /* Actions.swift */,
|
||||||
);
|
);
|
||||||
path = Core;
|
path = Core;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -234,13 +419,18 @@
|
|||||||
C4F7807A25D7F84B000DBC97 /* phpmon-tests */ = {
|
C4F7807A25D7F84B000DBC97 /* phpmon-tests */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
C4AF9F70275445FF00D44ED0 /* valet-config.json */,
|
||||||
C43A8A1F25D9D1D700591B77 /* brew.json */,
|
C43A8A1F25D9D1D700591B77 /* brew.json */,
|
||||||
C4F780A725D80AE8000DBC97 /* php.ini */,
|
C4F780A725D80AE8000DBC97 /* php.ini */,
|
||||||
C4F7807D25D7F84B000DBC97 /* Info.plist */,
|
C4F7807D25D7F84B000DBC97 /* Info.plist */,
|
||||||
C4F7809B25D80344000DBC97 /* CommandTest.swift */,
|
C4F7809B25D80344000DBC97 /* CommandTest.swift */,
|
||||||
C4F780AD25D80B37000DBC97 /* ExtensionParserTest.swift */,
|
C4F780AD25D80B37000DBC97 /* ExtensionParserTest.swift */,
|
||||||
C43A8A2325D9D20D00591B77 /* BrewJsonParserTest.swift */,
|
C43A8A2325D9D20D00591B77 /* BrewJsonParserTest.swift */,
|
||||||
|
C4FBFC512616485F00CDB8E1 /* PhpVersionDetectionTest.swift */,
|
||||||
C43A8A1925D9CD1000591B77 /* Utility.swift */,
|
C43A8A1925D9CD1000591B77 /* Utility.swift */,
|
||||||
|
C4AF9F76275447F100D44ED0 /* ValetConfigParserTest.swift */,
|
||||||
|
C4AF9F7C275454A900D44ED0 /* ValetTest.swift */,
|
||||||
|
C4B56360276AB0A500F12CCB /* VersionExtractorTest.swift */,
|
||||||
);
|
);
|
||||||
path = "phpmon-tests";
|
path = "phpmon-tests";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -251,8 +441,6 @@
|
|||||||
C49EAB45259FC305007F6C3B /* Paths.swift */,
|
C49EAB45259FC305007F6C3B /* Paths.swift */,
|
||||||
C42295DC2358D02000E263B2 /* Command.swift */,
|
C42295DC2358D02000E263B2 /* Command.swift */,
|
||||||
C41C1B4622B009A400E7CF16 /* Shell.swift */,
|
C41C1B4622B009A400E7CF16 /* Shell.swift */,
|
||||||
C4D8016522B1584700C6DA1B /* Startup.swift */,
|
|
||||||
C41C1B4C22B0215A00E7CF16 /* Actions.swift */,
|
|
||||||
);
|
);
|
||||||
path = Terminal;
|
path = Terminal;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -263,6 +451,7 @@
|
|||||||
C4F8C0A322D4F12C002EFE61 /* DateExtension.swift */,
|
C4F8C0A322D4F12C002EFE61 /* DateExtension.swift */,
|
||||||
C46FA23E246C358E00944F05 /* StringExtension.swift */,
|
C46FA23E246C358E00944F05 /* StringExtension.swift */,
|
||||||
C48D0C9225CC804200CC7490 /* XibLoadable.swift */,
|
C48D0C9225CC804200CC7490 /* XibLoadable.swift */,
|
||||||
|
C42759662627662800093CAE /* NSMenuExtension.swift */,
|
||||||
);
|
);
|
||||||
path = Extensions;
|
path = Extensions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -283,6 +472,9 @@
|
|||||||
dependencies = (
|
dependencies = (
|
||||||
);
|
);
|
||||||
name = "PHP Monitor";
|
name = "PHP Monitor";
|
||||||
|
packageProductDependencies = (
|
||||||
|
C4998F0526175E7200B2526E /* HotKey */,
|
||||||
|
);
|
||||||
productName = phpmon;
|
productName = phpmon;
|
||||||
productReference = C41C1B3322B0097F00E7CF16 /* PHP Monitor.app */;
|
productReference = C41C1B3322B0097F00E7CF16 /* PHP Monitor.app */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
@ -333,6 +525,9 @@
|
|||||||
Base,
|
Base,
|
||||||
);
|
);
|
||||||
mainGroup = C41C1B2A22B0097F00E7CF16;
|
mainGroup = C41C1B2A22B0097F00E7CF16;
|
||||||
|
packageReferences = (
|
||||||
|
C4998F0426175E7200B2526E /* XCRemoteSwiftPackageReference "HotKey" */,
|
||||||
|
);
|
||||||
productRefGroup = C41C1B3422B0097F00E7CF16 /* Products */;
|
productRefGroup = C41C1B3422B0097F00E7CF16 /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
@ -350,9 +545,13 @@
|
|||||||
files = (
|
files = (
|
||||||
C41C1B3B22B0098000E7CF16 /* Assets.xcassets in Resources */,
|
C41C1B3B22B0098000E7CF16 /* Assets.xcassets in Resources */,
|
||||||
C41C1B3E22B0098000E7CF16 /* Main.storyboard in Resources */,
|
C41C1B3E22B0098000E7CF16 /* Main.storyboard in Resources */,
|
||||||
|
C4AF9F71275445FF00D44ED0 /* valet-config.json in Resources */,
|
||||||
C48D0C9025CC7FD000CC7490 /* StatsView.xib in Resources */,
|
C48D0C9025CC7FD000CC7490 /* StatsView.xib in Resources */,
|
||||||
C405A4D124B9B9140062FAFA /* InternetAccessPolicy.plist in Resources */,
|
C405A4D124B9B9140062FAFA /* InternetAccessPolicy.plist in Resources */,
|
||||||
|
C4232EE52612526500158FC6 /* Credits.html in Resources */,
|
||||||
|
54FCFD26276C883F004CE748 /* CheckboxPreferenceView.xib in Resources */,
|
||||||
C473319F2470923A009A0597 /* Localizable.strings in Resources */,
|
C473319F2470923A009A0597 /* Localizable.strings in Resources */,
|
||||||
|
54FCFD2D276C8D67004CE748 /* HotkeyPreferenceView.xib in Resources */,
|
||||||
C405A4D024B9B9140062FAFA /* InternetAccessPolicy.strings in Resources */,
|
C405A4D024B9B9140062FAFA /* InternetAccessPolicy.strings in Resources */,
|
||||||
C48D0C9A25CC888B00CC7490 /* HeaderView.xib in Resources */,
|
C48D0C9A25CC888B00CC7490 /* HeaderView.xib in Resources */,
|
||||||
);
|
);
|
||||||
@ -362,8 +561,11 @@
|
|||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
54FCFD27276C883F004CE748 /* CheckboxPreferenceView.xib in Resources */,
|
||||||
|
54FCFD2E276C8D67004CE748 /* HotkeyPreferenceView.xib in Resources */,
|
||||||
C4F780A825D80AE8000DBC97 /* php.ini in Resources */,
|
C4F780A825D80AE8000DBC97 /* php.ini in Resources */,
|
||||||
C43A8A2025D9D1D700591B77 /* brew.json in Resources */,
|
C43A8A2025D9D1D700591B77 /* brew.json in Resources */,
|
||||||
|
C4AF9F72275445FF00D44ED0 /* valet-config.json in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@ -376,24 +578,48 @@
|
|||||||
files = (
|
files = (
|
||||||
C4ACA38F25C754C100060C66 /* PhpExtension.swift in Sources */,
|
C4ACA38F25C754C100060C66 /* PhpExtension.swift in Sources */,
|
||||||
C4D8016622B1584700C6DA1B /* Startup.swift in Sources */,
|
C4D8016622B1584700C6DA1B /* Startup.swift in Sources */,
|
||||||
|
C4998F0A2617633900B2526E /* PrefsWC.swift in Sources */,
|
||||||
C4F8C0A422D4F12C002EFE61 /* DateExtension.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 */,
|
C41C1B4722B009A400E7CF16 /* Shell.swift in Sources */,
|
||||||
|
C4F2E43A2752F7D00020E974 /* PhpInstallation.swift in Sources */,
|
||||||
C41C1B4D22B0215A00E7CF16 /* Actions.swift in Sources */,
|
C41C1B4D22B0215A00E7CF16 /* Actions.swift in Sources */,
|
||||||
|
C41E871A2763D42300161EE0 /* SiteListVC+ContextMenu.swift in Sources */,
|
||||||
C48D0CA325CC992000CC7490 /* StatsView.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 */,
|
C42295DD2358D02000E263B2 /* Command.swift in Sources */,
|
||||||
|
54B48B5F275F66AE006D90C5 /* Application.swift in Sources */,
|
||||||
|
C4B97B78275CF1B5003F3378 /* App+ActivationPolicy.swift in Sources */,
|
||||||
C4811D2422D70A4700B5F6B3 /* App.swift in Sources */,
|
C4811D2422D70A4700B5F6B3 /* App.swift in Sources */,
|
||||||
C41C1B4922B00A9800E7CF16 /* MenuBarImageGenerator.swift in Sources */,
|
C41C1B4922B00A9800E7CF16 /* MenuBarImageGenerator.swift in Sources */,
|
||||||
|
5420395F2613607600FB00FA /* Preferences.swift in Sources */,
|
||||||
C48D0C9325CC804200CC7490 /* XibLoadable.swift in Sources */,
|
C48D0C9325CC804200CC7490 /* XibLoadable.swift in Sources */,
|
||||||
|
54FCFD2A276C8AA4004CE748 /* CheckboxPreferenceView.swift in Sources */,
|
||||||
C4811D2A22D70F9A00B5F6B3 /* MainMenu.swift in Sources */,
|
C4811D2A22D70F9A00B5F6B3 /* MainMenu.swift in Sources */,
|
||||||
|
C41CA5ED2774F8EE00A2C80E /* SiteListVC+Actions.swift in Sources */,
|
||||||
C412E5FC25700D5300A1FB67 /* HomebrewPackage.swift in Sources */,
|
C412E5FC25700D5300A1FB67 /* HomebrewPackage.swift in Sources */,
|
||||||
|
54AB03262763858F00A29D5F /* Timer.swift in Sources */,
|
||||||
C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */,
|
C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */,
|
||||||
C41C1B4B22B019FF00E7CF16 /* PhpInstallation.swift in Sources */,
|
C42759672627662800093CAE /* NSMenuExtension.swift in Sources */,
|
||||||
|
C464ADAF275A7A69003FCD53 /* SiteListVC.swift in Sources */,
|
||||||
|
C41C1B4B22B019FF00E7CF16 /* ActivePhpInstallation.swift in Sources */,
|
||||||
C49EAB46259FC305007F6C3B /* Paths.swift in Sources */,
|
C49EAB46259FC305007F6C3B /* Paths.swift in Sources */,
|
||||||
|
C4188989275FE8CB001EF227 /* Filesystem.swift in Sources */,
|
||||||
|
C4B97B7B275CF20A003F3378 /* App+GlobalHotkey.swift in Sources */,
|
||||||
C476FF9822B0DD830098105B /* Alert.swift in Sources */,
|
C476FF9822B0DD830098105B /* Alert.swift in Sources */,
|
||||||
C474B00624C0E98C00066A22 /* LocalNotification.swift in Sources */,
|
C474B00624C0E98C00066A22 /* LocalNotification.swift in Sources */,
|
||||||
C48D0C9625CC80B100CC7490 /* HeaderView.swift in Sources */,
|
C48D0C9625CC80B100CC7490 /* HeaderView.swift in Sources */,
|
||||||
|
C4B5635E276AB09000F12CCB /* VersionExtractor.swift in Sources */,
|
||||||
C47331A2247093B7009A0597 /* StatusMenu.swift in Sources */,
|
C47331A2247093B7009A0597 /* StatusMenu.swift in Sources */,
|
||||||
C46FA23F246C358E00944F05 /* StringExtension.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 */,
|
C4EE188422D3386B00E126E5 /* Constants.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@ -402,28 +628,56 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
54EAC806262F212B0092D14E /* GlobalKeybindPreference.swift in Sources */,
|
||||||
|
C41CA5EE2774F8EE00A2C80E /* SiteListVC+Actions.swift in Sources */,
|
||||||
C4F780C425D80B75000DBC97 /* MainMenu.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 */,
|
C4F780C825D80B75000DBC97 /* DateExtension.swift in Sources */,
|
||||||
C4F780CC25D80B75000DBC97 /* PhpInstallation.swift in Sources */,
|
C4F780CC25D80B75000DBC97 /* ActivePhpInstallation.swift in Sources */,
|
||||||
C4F780B125D80B4D000DBC97 /* PhpExtension.swift in Sources */,
|
C4F780B125D80B4D000DBC97 /* PhpExtension.swift in Sources */,
|
||||||
C4F780CE25D80B75000DBC97 /* LocalNotification.swift in Sources */,
|
C4F780CE25D80B75000DBC97 /* LocalNotification.swift in Sources */,
|
||||||
|
C4FBFC532616485F00CDB8E1 /* PhpVersionDetectionTest.swift in Sources */,
|
||||||
C43A8A2425D9D20D00591B77 /* BrewJsonParserTest.swift in Sources */,
|
C43A8A2425D9D20D00591B77 /* BrewJsonParserTest.swift in Sources */,
|
||||||
C4F780CA25D80B75000DBC97 /* HomebrewPackage.swift in Sources */,
|
C4F780CA25D80B75000DBC97 /* HomebrewPackage.swift in Sources */,
|
||||||
|
C4AF9F7B2754499000D44ED0 /* Valet.swift in Sources */,
|
||||||
C4F780C025D80B6E000DBC97 /* Startup.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 */,
|
C4F780AE25D80B37000DBC97 /* ExtensionParserTest.swift in Sources */,
|
||||||
C4F780C725D80B75000DBC97 /* StatusMenu.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 */,
|
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 */,
|
C4F7809C25D80344000DBC97 /* CommandTest.swift in Sources */,
|
||||||
C4F780BA25D80B62000DBC97 /* AppDelegate.swift in Sources */,
|
C4F780BA25D80B62000DBC97 /* AppDelegate.swift in Sources */,
|
||||||
|
54FCFD31276C8DA4004CE748 /* HotkeyPreferenceView.swift in Sources */,
|
||||||
|
C4998F0B2617633900B2526E /* PrefsWC.swift in Sources */,
|
||||||
C4F780A225D804AA000DBC97 /* Paths.swift in Sources */,
|
C4F780A225D804AA000DBC97 /* Paths.swift in Sources */,
|
||||||
|
C4F2E43B27530F750020E974 /* PhpInstallation.swift in Sources */,
|
||||||
C4F780BD25D80B65000DBC97 /* Constants.swift in Sources */,
|
C4F780BD25D80B65000DBC97 /* Constants.swift in Sources */,
|
||||||
C4F780C325D80B75000DBC97 /* HeaderView.swift in Sources */,
|
C4F780C325D80B75000DBC97 /* HeaderView.swift in Sources */,
|
||||||
C4F7809625D7FBF8000DBC97 /* Shell.swift in Sources */,
|
C4F7809625D7FBF8000DBC97 /* Shell.swift in Sources */,
|
||||||
|
C4AF9F7D275454A900D44ED0 /* ValetTest.swift in Sources */,
|
||||||
|
C4B56362276AB0A500F12CCB /* VersionExtractorTest.swift in Sources */,
|
||||||
C4F780C525D80B75000DBC97 /* MenuBarImageGenerator.swift in Sources */,
|
C4F780C525D80B75000DBC97 /* MenuBarImageGenerator.swift in Sources */,
|
||||||
C4F780B725D80B5D000DBC97 /* App.swift in Sources */,
|
C4F780B725D80B5D000DBC97 /* App.swift in Sources */,
|
||||||
C4F780C925D80B75000DBC97 /* StringExtension.swift in Sources */,
|
C4F780C925D80B75000DBC97 /* StringExtension.swift in Sources */,
|
||||||
|
C481F79A26164A7C004FBCFF /* Preferences.swift in Sources */,
|
||||||
|
C464ADAD275A7A3F003FCD53 /* SiteListWC.swift in Sources */,
|
||||||
C4F780CB25D80B75000DBC97 /* StatsView.swift in Sources */,
|
C4F780CB25D80B75000DBC97 /* StatsView.swift in Sources */,
|
||||||
|
C464ADB0275A7A6A003FCD53 /* SiteListVC.swift in Sources */,
|
||||||
C43A8A1A25D9CD1000591B77 /* Utility.swift in Sources */,
|
C43A8A1A25D9CD1000591B77 /* Utility.swift in Sources */,
|
||||||
|
C418898A275FE8CB001EF227 /* Filesystem.swift in Sources */,
|
||||||
C4F780C625D80B75000DBC97 /* XibLoadable.swift in Sources */,
|
C4F780C625D80B75000DBC97 /* XibLoadable.swift in Sources */,
|
||||||
C4F7809F25D8037C000DBC97 /* Command.swift in Sources */,
|
C4F7809F25D8037C000DBC97 /* Command.swift in Sources */,
|
||||||
C4F780B425D80B51000DBC97 /* Actions.swift in Sources */,
|
C4F780B425D80B51000DBC97 /* Actions.swift in Sources */,
|
||||||
@ -504,7 +758,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
@ -560,7 +814,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
@ -577,7 +831,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 48;
|
CURRENT_PROJECT_VERSION = 137;
|
||||||
DEVELOPMENT_TEAM = 8M54J5J787;
|
DEVELOPMENT_TEAM = 8M54J5J787;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
INFOPLIST_FILE = phpmon/Info.plist;
|
INFOPLIST_FILE = phpmon/Info.plist;
|
||||||
@ -585,7 +839,8 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 3.1;
|
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||||
|
MARKETING_VERSION = 4.1.2;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
|
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@ -601,7 +856,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 48;
|
CURRENT_PROJECT_VERSION = 137;
|
||||||
DEVELOPMENT_TEAM = 8M54J5J787;
|
DEVELOPMENT_TEAM = 8M54J5J787;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
INFOPLIST_FILE = phpmon/Info.plist;
|
INFOPLIST_FILE = phpmon/Info.plist;
|
||||||
@ -609,7 +864,8 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 3.1;
|
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||||
|
MARKETING_VERSION = 4.1.2;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
|
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@ -630,7 +886,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
"@loader_path/../Frameworks",
|
"@loader_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 11.1;
|
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon-tests";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon-tests";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
@ -651,7 +907,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
"@loader_path/../Frameworks",
|
"@loader_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 11.1;
|
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon-tests";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon-tests";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
@ -690,6 +946,25 @@
|
|||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
|
/* Begin XCRemoteSwiftPackageReference section */
|
||||||
|
C4998F0426175E7200B2526E /* XCRemoteSwiftPackageReference "HotKey" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/soffes/HotKey";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMinorVersion;
|
||||||
|
minimumVersion = 0.1.3;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
C4998F0526175E7200B2526E /* HotKey */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = C4998F0426175E7200B2526E /* XCRemoteSwiftPackageReference "HotKey" */;
|
||||||
|
productName = HotKey;
|
||||||
|
};
|
||||||
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = C41C1B2B22B0097F00E7CF16 /* Project object */;
|
rootObject = C41C1B2B22B0097F00E7CF16 /* Project object */;
|
||||||
}
|
}
|
||||||
|
150
README.md
@ -1,33 +1,36 @@
|
|||||||
# PHP Monitor
|
# PHP Monitor
|
||||||
|
|
||||||
|
> If this software has been useful to you, I ask that you **please star the repository**, that way I know that the software is being used. Also, please consider leaving [a one-time donation](https://nicoverbruggen.be/sponsor) to support the project.
|
||||||
|
> You can also send me [feedback](https://twitter.com/nicoverbruggen) if the app came in handy.<br>**Thank you!** ⭐️
|
||||||
|
|
||||||
<img src="./phpmon/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png" alt="phpmon icon" width="128px" />
|
<img src="./phpmon/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png" alt="phpmon icon" width="128px" />
|
||||||
|
|
||||||
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.
|
**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/screenshot.png" width="389px" 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>
|
<small><i>Screenshot: A menu showing all of the functionality of PHP Monitor.</i></small>
|
||||||
|
|
||||||
It's also super convenient to switch between different versions of PHP. You'll even get notifications (only if you choose to opt-in, of course)!
|
It's super convenient to switch between different versions of PHP. You'll even get notifications (only if you choose to opt-in, of course)!
|
||||||
|
|
||||||
<img src="./docs/notification.png" width="370px" alt="phpmon screenshot (notification)"/>
|
<img src="./docs/notification.png" width="370px" alt="phpmon screenshot (notification)"/>
|
||||||
|
|
||||||
It also gives you quick access to various useful functionality (like accessing configuration files, restarting services, and more).
|
PHP Monitor also gives you quick access to various useful functionality (like accessing configuration files, restarting services, and more).
|
||||||
|
|
||||||
## 🖥 System requirements
|
## 🖥 System requirements
|
||||||
|
|
||||||
PHP Monitor is a universal application that runs on Apple Silicon **and** Intel-based Macs.
|
PHP Monitor is a universal application that runs on Apple Silicon **and** Intel-based Macs.
|
||||||
|
|
||||||
* macOS 10.14 Mojave or higher (works on macOS 11 Big Sur)
|
* macOS 11 Big Sur or higher (supports macOS 12 Monterey)
|
||||||
* Homebrew is installed in `/usr/local/homebrew` or `/opt/homebrew`
|
* Homebrew is installed in `/usr/local/homebrew` or `/opt/homebrew`
|
||||||
* The brew formula `php` has to be installed (which version is detected)
|
* 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
|
## 🚀 How to install
|
||||||
|
|
||||||
You can install via Homebrew, or may download the latest [release](https://github.com/nicoverbruggen/phpmon/releases).
|
You can install via Homebrew (recommended), or may download the latest release on GitHub.
|
||||||
|
|
||||||
To install via Homebrew, run:
|
To install via Homebrew, run:
|
||||||
|
|
||||||
@ -40,10 +43,6 @@ To upgrade your existing installation, run:
|
|||||||
|
|
||||||
_The app is signed and notarized, meaning all you have to do is approve its first launch._
|
_The app is signed and notarized, meaning all you have to do is approve its first launch._
|
||||||
|
|
||||||
## ⭐️ Star me!
|
|
||||||
|
|
||||||
If this software has been useful to you, all I ask is that you **please star the repository**, so I know that the software is being used. You can also send me [feedback](https://twitter.com/nicoverbruggen) if the app came in handy. 😃
|
|
||||||
|
|
||||||
## 👨💻 Why build this?
|
## 👨💻 Why build this?
|
||||||
|
|
||||||
I wanted to be able to **see at a glance** which version of PHP was linked, and handle dealing with Laravel Valet in a simple app without having to deal with the terminal every time.
|
I wanted to be able to **see at a glance** which version of PHP was linked, and handle dealing with Laravel Valet in a simple app without having to deal with the terminal every time.
|
||||||
@ -60,7 +59,26 @@ PHP Monitor performs some integrity checks to ensure a good experience when usin
|
|||||||
|
|
||||||
> If you are having issues, the first thing you should be doing is installing the latest version of PHP Monitor _and_ Laravel Valet. This can resolve a variety of issues. To upgrade Valet, run `composer global update`. Don't forget to run `valet install` after upgrading.
|
> If you are having issues, the first thing you should be doing is installing the latest version of PHP Monitor _and_ Laravel Valet. This can resolve a variety of issues. To upgrade Valet, run `composer global update`. Don't forget to run `valet install` after upgrading.
|
||||||
|
|
||||||
If you're still having issues, here's a few common issues and solutions:
|
If you're still having issues, here's a few common questions & answers, as well as issues and solutions:
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>Which versions of PHP are supported?</strong></summary>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>PHP 5.6</li>
|
||||||
|
<li>PHP 7.0</li>
|
||||||
|
<li>PHP 7.1</li>
|
||||||
|
<li>PHP 7.2</li>
|
||||||
|
<li>PHP 7.3</li>
|
||||||
|
<li>PHP 7.4</li>
|
||||||
|
<li>PHP 8.0</li>
|
||||||
|
<li>PHP 8.1</li>
|
||||||
|
<li>PHP 8.2 (experimental)</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
For more details, consult the [constants file](https://github.com/nicoverbruggen/phpmon/blob/main/phpmon/Constants.swift#L16) file to see which versions are supported.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><strong>I want PHP Monitor to start up when I boot my Mac!</strong></summary>
|
<summary><strong>I want PHP Monitor to start up when I boot my Mac!</strong></summary>
|
||||||
@ -93,9 +111,15 @@ If you're on an Apple Silicon-based Mac, you'll need to add:
|
|||||||
# on an M1 Mac
|
# on an M1 Mac
|
||||||
export PATH=$HOME/bin:/opt/homebrew/bin:$PATH
|
export PATH=$HOME/bin:/opt/homebrew/bin:$PATH
|
||||||
|
|
||||||
and add the following to your .zshrc:
|
and add the following to your .zshrc, but add this BEFORE the homebrew PATH additions:
|
||||||
|
|
||||||
export PATH=$HOME/bin:~/.composer/vendor/bin:$PATH
|
export PATH=$HOME/bin:~/.composer/vendor/bin:$PATH
|
||||||
|
|
||||||
|
If you're adding composer and Homebrew binaries, ensure that Homebrew binaries are preferred by adding these to the path last. On my system, that looks like this:
|
||||||
|
|
||||||
|
export PATH=$HOME/bin:/usr/local/bin:$PATH
|
||||||
|
export PATH=$HOME/bin:~/.composer/vendor/bin:$PATH
|
||||||
|
export PATH=$HOME/bin:/opt/homebrew/bin:$PATH
|
||||||
|
|
||||||
Make sure PHP is linked correctly:
|
Make sure PHP is linked correctly:
|
||||||
|
|
||||||
@ -136,6 +160,35 @@ This problem is usually resolved by upgrading Valet and running `valet install`
|
|||||||
valet install
|
valet install
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>PHP Monitor tells me my installation is broken, but I don't see why!</strong></summary>
|
||||||
|
|
||||||
|
PHP Monitor tells you that a PHP installation is broken, if the configuration is causing warnings or errors when determining the version number.
|
||||||
|
|
||||||
|
Since PHP Monitor changes the linked version via Homebrew, both Valet *and* your terminal (CLI) should use the new PHP version.
|
||||||
|
|
||||||
|
However, this might not be the case on your system. You _might_ have a specific version of PHP linked if that is not the case. In that case, you may need to change your `.bashrc` or `.zshrc` file where the PATH is set (depending on the terminal you use).
|
||||||
|
|
||||||
|
You can find out which version of PHP is being used by running `which php`.
|
||||||
|
|
||||||
|
You can find out what exactly is causing the issue by running a command. On Intel, you can run (replace `7.4` with the version that is broken):
|
||||||
|
|
||||||
|
```
|
||||||
|
/usr/local/opt/php@7.4/bin/php -r "print phpversion();"
|
||||||
|
```
|
||||||
|
|
||||||
|
On Apple Silicon, you can run (replace `7.4` with the version that is broken):
|
||||||
|
|
||||||
|
```
|
||||||
|
/opt/homebrew/opt/php@7.4/bin/php -r "print phpversion();"
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see an error or a warning here in the output.
|
||||||
|
|
||||||
|
Usually this is a duplicate extension declaration causing issues, or an extension that couldn't be loaded. You'll have to solve that issue yourself (usually by removing the offending extension or reinstalling).
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><strong>One of the limits (memory limit, max POST size, max upload size) shows an exclamation mark!</strong></summary>
|
<summary><strong>One of the limits (memory limit, max POST size, max upload size) shows an exclamation mark!</strong></summary>
|
||||||
@ -151,7 +204,7 @@ You must a provide a value like so: `1024K`, `256M`, `1G`. Alternatively, `-1` i
|
|||||||
<details>
|
<details>
|
||||||
<summary><strong>One of my commented out extensions is not being detected...</strong></summary>
|
<summary><strong>One of my commented out extensions is not being detected...</strong></summary>
|
||||||
|
|
||||||
The app searches in the relevant `php.ini` file for a specific pattern. For regular extensions:
|
The app searches in the relevant `.ini` files for a specific pattern. For regular extensions:
|
||||||
|
|
||||||
* `extension="*.so"`
|
* `extension="*.so"`
|
||||||
* `; extension="*.so"`
|
* `; extension="*.so"`
|
||||||
@ -162,6 +215,9 @@ For Zend extensions:
|
|||||||
* `; zend_extension="*.so"`
|
* `; zend_extension="*.so"`
|
||||||
|
|
||||||
The `*` is a wildcard and the name of the extension. If you've commented out the extension, make sure you've commented it out with a semicolon (;) and a single space after the semicolon for PHP Monitor to detect it.
|
The `*` is a wildcard and the name of the extension. If you've commented out the extension, make sure you've commented it out with a semicolon (;) and a single space after the semicolon for PHP Monitor to detect it.
|
||||||
|
|
||||||
|
Since v3.4 all of the loaded .ini files are sourced to determine which extensions are enabled.
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@ -181,27 +237,63 @@ PHP Monitor itself doesn't do any network requests. Feel free to check the sourc
|
|||||||
</details>
|
</details>
|
||||||
|
|
||||||
<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>
|
||||||
|
|
||||||
## 📝 Another issue?
|
<details>
|
||||||
|
<summary><strong>The app has crashed!</strong></summary>
|
||||||
|
|
||||||
|
Please get in touch and open an issue. PHP Monitor shouldn't crash... (unless you are actually removing PHP *while* the app is running, that’s considered normal behaviour!)
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## 📝 Having another issue?
|
||||||
|
|
||||||
I did not include any tracking or analytics software, so if you encounter issues, let me know [via an issue](https://github.com/nicoverbruggen/phpmon/issues/new).
|
I did not include any tracking or analytics software, so if you encounter issues, let me know [via an issue](https://github.com/nicoverbruggen/phpmon/issues/new).
|
||||||
|
|
||||||
## 💵 Support me?
|
## 💵 Support me?
|
||||||
|
|
||||||
I usually develop this application in my spare time, after work. If you find the application useful and you have a bit of money to spare, feel free to send me [a tip via PayPal](https://paypal.me/nicoverbruggen).
|
PHP Monitor is available entirely **free of charge**, but if you can afford it a donation helps keep the project alive and the app maintained.
|
||||||
|
|
||||||
|
You can find a [sponsor](https://nicoverbruggen.be/sponsor) link at the top of this repo or you could click the link here to be taken to my sponsorship page.
|
||||||
|
|
||||||
|
Donations really help with the Apple Developer Program cost, and keep me motivated to keep working on PHP Monitor outside of work hours (I do have a day job!).
|
||||||
|
|
||||||
|
## 😎 Acknowledgements
|
||||||
|
|
||||||
|
While I did make this application during my own free time, I have been lucky enough to do various experiments during work hours at [DIVE](https://dive.be). I'd also like to shout out the following folks:
|
||||||
|
|
||||||
|
* My colleagues at [DIVE](https://dive.be)
|
||||||
|
* The [Homebrew](https://brew.sh/) team who maintain
|
||||||
|
* The [developers & maintainers of Valet](https://github.com/laravel/valet/graphs/contributors)
|
||||||
|
* Everyone in the Laravel community who shared the app (thanks!)
|
||||||
|
* Various folks who [reached](https://twitter.com/stauffermatt) [out](https://twitter.com/marcelpociot)
|
||||||
|
* Everyone who left feedback via issues
|
||||||
|
|
||||||
|
Thank you very much for your contributions, kind words and support.
|
||||||
|
|
||||||
## 🚜 How it works
|
## 🚜 How it works
|
||||||
|
|
||||||
### Loading info about PHP in the background
|
### Loading info about PHP in the background
|
||||||
|
|
||||||
This utility runs `php -r 'print phpversion()'` in the background periodically. It also checks your `.ini` files for extensions and loads more information about your limits (memory limit, POST limit, upload limit).
|
This utility runs `php-config --version` in the background periodically. It also checks your `.ini` files for extensions and loads more information about your limits (memory limit, POST limit, upload limit).
|
||||||
|
|
||||||
In order to save power, this only happens once every 60 seconds.
|
In order to save power, this only happens once every 60 seconds.
|
||||||
|
|
||||||
@ -209,19 +301,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 utility will detect which PHP versions you have installed via Homebrew, and then allows you to switch between them.
|
||||||
|
|
||||||
This means:
|
The switcher will disable all PHP-FPM services not belonging to the version you wish to use, and link the desired version of PHP. Then, it'll restart your desired PHP version's FPM process. This all happens in parallel, so this should be much faster than Valet’s switcher.
|
||||||
|
|
||||||
- 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
|
|
||||||
- Switch to whatever version of PHP `php` is at (this is done to ensure that Valet works, even when attempting to use PHP 5.6)
|
|
||||||
- Stop all relevant services (`php`, `nginx`)
|
|
||||||
- Link the desired version of PHP
|
|
||||||
- Start the correct `php` service for the desired PHP version
|
|
||||||
|
|
||||||
### Want to know more?
|
### Want to know more?
|
||||||
|
|
||||||
@ -231,7 +311,7 @@ This app isn't very complicated after all. In the end, this just (conveniently)
|
|||||||
|
|
||||||
## 🔧 Build instructions
|
## 🔧 Build instructions
|
||||||
|
|
||||||
<img src="./docs/build.png" width="320px" alt="build button in Xcode"/>
|
<img src="./docs/build.png" width="404px" alt="build button in Xcode"/>
|
||||||
|
|
||||||
If you'd like to build PHP Monitor yourself, you need:
|
If you'd like to build PHP Monitor yourself, you need:
|
||||||
|
|
||||||
|
26
SECURITY.md
@ -2,15 +2,25 @@
|
|||||||
|
|
||||||
## Supported versions
|
## 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 |
|
| Version | Apple silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Minimum Required Valet Version |
|
||||||
| ------- | ------------- | ------------------ | ----- | ----- |
|
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
||||||
| 3.0 | ✅ Universal binary | ✅ | Big Sur (11.0) | macOS 10.14+ |
|
| 4.1 | ✅ Universal binary | ✅ Yes | Big Sur (11.0) and Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | 2.16.2 |
|
||||||
| 2.6 | ✅ Universal binary | ❌ | Big Sur (11.0) | macOS 10.14+ |
|
|
||||||
| 2.5 | ✴️ Universal binary<br/>`/usr/local/homebrew` installations only | ❌ | Big Sur (11.0)<br/>Catalina (10.15) | macOS 10.14+ |
|
## Legacy versions
|
||||||
| 2.4 | ✴️ Universal binary<br/>`/usr/local/homebrew` installations only | ❌ | Big Sur (11.0)<br/>Catalina (10.15) | macOS 10.14+ |
|
|
||||||
| < 2.4 | ❌ Intel binary<br/>`/usr/local/homebrew` installations only | ❌ | Catalina (10.15) | macOS 10.14+ |
|
These versions of PHP Monitor are no longer supported, but if you’re using an older computer with an older version of Homebrew, Valet or macOS, you might want to use one of these versions.
|
||||||
|
|
||||||
|
| Version | Apple silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Minimum Required Valet Version |
|
||||||
|
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
||||||
|
| 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
|
## Reporting a vulnerability
|
||||||
|
|
||||||
|
BIN
docs/build.png
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 127 KiB |
BIN
docs/screenshot41.jpg
Normal file
After Width: | Height: | Size: 396 KiB |
@ -23,6 +23,9 @@ class BrewJsonParserTest: XCTestCase {
|
|||||||
XCTAssertEqual(package.name, "php")
|
XCTAssertEqual(package.name, "php")
|
||||||
XCTAssertEqual(package.full_name, "php")
|
XCTAssertEqual(package.full_name, "php")
|
||||||
XCTAssertEqual(package.aliases.first!, "php@8.0")
|
XCTAssertEqual(package.aliases.first!, "php@8.0")
|
||||||
|
XCTAssertEqual(package.installed.contains(where: { installed in
|
||||||
|
installed.version.starts(with: "8.0")
|
||||||
|
}), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -23,24 +23,40 @@ class ExtensionParserTest: XCTestCase {
|
|||||||
func testExtensionNameIsCorrect() throws {
|
func testExtensionNameIsCorrect() throws {
|
||||||
let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
|
let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
|
||||||
|
|
||||||
XCTAssertEqual(extensions.first!.name, "xdebug")
|
let extensionNames = extensions.map { (ext) -> String in
|
||||||
XCTAssertEqual(extensions.last!.name, "imagick")
|
return ext.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// These 6 should be found
|
||||||
|
XCTAssertTrue(extensionNames.contains("xdebug"))
|
||||||
|
XCTAssertTrue(extensionNames.contains("imagick"))
|
||||||
|
XCTAssertTrue(extensionNames.contains("sodium-next"))
|
||||||
|
XCTAssertTrue(extensionNames.contains("opcache"))
|
||||||
|
XCTAssertTrue(extensionNames.contains("yaml"))
|
||||||
|
XCTAssertTrue(extensionNames.contains("custom"))
|
||||||
|
|
||||||
|
XCTAssertFalse(extensionNames.contains("fake"))
|
||||||
|
XCTAssertFalse(extensionNames.contains("nice"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testExtensionStatusIsCorrect() throws {
|
func testExtensionStatusIsCorrect() throws {
|
||||||
let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
|
let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
|
||||||
|
|
||||||
XCTAssertEqual(extensions.first!.enabled, true)
|
// xdebug should be enabled
|
||||||
XCTAssertEqual(extensions.last!.enabled, false)
|
XCTAssertEqual(extensions[0].enabled, true)
|
||||||
|
|
||||||
|
// imagick should be disabled
|
||||||
|
XCTAssertEqual(extensions[1].enabled, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testToggleWorksAsExpected() throws {
|
func testToggleWorksAsExpected() throws {
|
||||||
let destination = Utility.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")!
|
let destination = Utility.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")!
|
||||||
let extensions = PhpExtension.load(from: destination)
|
let extensions = PhpExtension.load(from: destination)
|
||||||
XCTAssertEqual(extensions.count, 2)
|
XCTAssertEqual(extensions.count, 6)
|
||||||
|
|
||||||
// Try to disable it!
|
// Try to disable xdebug (should be detected first)!
|
||||||
let xdebug = extensions.first!
|
let xdebug = extensions.first!
|
||||||
|
XCTAssertTrue(xdebug.name == "xdebug")
|
||||||
XCTAssertEqual(xdebug.enabled, true)
|
XCTAssertEqual(xdebug.enabled, true)
|
||||||
xdebug.toggle()
|
xdebug.toggle()
|
||||||
XCTAssertEqual(xdebug.enabled, false)
|
XCTAssertEqual(xdebug.enabled, false)
|
||||||
|
30
phpmon-tests/PhpVersionDetectionTest.swift
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
//
|
||||||
|
// PhpVersionDetectionTest.swift
|
||||||
|
// phpmon-tests
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 01/04/2021.
|
||||||
|
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
class PhpVersionDetectionTest: XCTestCase {
|
||||||
|
|
||||||
|
func testCanDetectValidPhpVersions() throws {
|
||||||
|
let outcome = Actions.extractPhpVersions(from: [
|
||||||
|
"", // empty lines should be omitted
|
||||||
|
"php@8.0",
|
||||||
|
"php@8.0", // should only be detected once
|
||||||
|
"meta-php@8.0", // should be omitted, invalid
|
||||||
|
"php@8.0-coolio", // should be omitted, invalid
|
||||||
|
"php@7.0",
|
||||||
|
"",
|
||||||
|
"unrelatedphp@1.0", // should be omitted, invalid
|
||||||
|
"php@5.6",
|
||||||
|
"php@5.4" // should be omitted, not supported
|
||||||
|
], checkBinaries: false)
|
||||||
|
|
||||||
|
XCTAssertEqual(outcome, ["8.0", "7.0", "5.6"])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
38
phpmon-tests/ValetConfigParserTest.swift
Normal 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
18
phpmon-tests/ValetTest.swift
Normal 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."))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
25
phpmon-tests/VersionExtractorTest.swift
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,5 +1,16 @@
|
|||||||
|
# These should be detected
|
||||||
|
|
||||||
zend_extension="xdebug.so"
|
zend_extension="xdebug.so"
|
||||||
; zend_extension="imagick.so"
|
; zend_extension="imagick.so"
|
||||||
|
zend_extension=/opt/homebrew/opt/php/lib/php/20200930/opcache.so
|
||||||
|
zend_extension="/opt/homebrew/opt/php/lib/php/20200930/yaml.so"
|
||||||
|
;zend_extension="sodium-next.so"
|
||||||
|
extension = custom.so
|
||||||
|
|
||||||
|
# These should not be detected
|
||||||
|
|
||||||
|
#zend_extension="/opt/homebrew/opt/php/lib/php/20200930/commented.so"
|
||||||
|
hextension = nice.so
|
||||||
|
|
||||||
[PHP]
|
[PHP]
|
||||||
|
|
||||||
|
8
phpmon-tests/valet-config.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"tld": "test",
|
||||||
|
"paths": [
|
||||||
|
"/Users/username/.config/valet/Sites",
|
||||||
|
"/Users/username/Sites"
|
||||||
|
],
|
||||||
|
"loopback": "127.0.0.1"
|
||||||
|
}
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 7.7 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 714 B After Width: | Height: | Size: 558 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 315 KiB After Width: | Height: | Size: 148 KiB |
68
phpmon/Assets.xcassets/AppIconBeta.appiconset/Contents.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
BIN
phpmon/Assets.xcassets/AppIconBeta.appiconset/icon_128x128.png
Normal file
After Width: | Height: | Size: 8.1 KiB |
After Width: | Height: | Size: 19 KiB |
BIN
phpmon/Assets.xcassets/AppIconBeta.appiconset/icon_16x16.png
Normal file
After Width: | Height: | Size: 585 B |
BIN
phpmon/Assets.xcassets/AppIconBeta.appiconset/icon_16x16@2x.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
phpmon/Assets.xcassets/AppIconBeta.appiconset/icon_256x256.png
Normal file
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 52 KiB |
BIN
phpmon/Assets.xcassets/AppIconBeta.appiconset/icon_32x32.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
phpmon/Assets.xcassets/AppIconBeta.appiconset/icon_32x32@2x.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
phpmon/Assets.xcassets/AppIconBeta.appiconset/icon_512x512.png
Normal file
After Width: | Height: | Size: 52 KiB |
After Width: | Height: | Size: 134 KiB |
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"info" : {
|
"info" : {
|
||||||
"version" : 1,
|
"author" : "xcode",
|
||||||
"author" : "xcode"
|
"version" : 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
24
phpmon/Assets.xcassets/IconLinked.imageset/Contents.json
vendored
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
1
phpmon/Assets.xcassets/IconLinked.imageset/link.svg
vendored
Normal 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 |
24
phpmon/Assets.xcassets/IconParked.imageset/Contents.json
vendored
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
1
phpmon/Assets.xcassets/IconParked.imageset/car-alt.svg
vendored
Normal 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 |
24
phpmon/Assets.xcassets/Lock.imageset/Contents.json
vendored
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
10
phpmon/Assets.xcassets/Lock.imageset/Locked.svg
vendored
Normal 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 |
24
phpmon/Assets.xcassets/LockUnlocked.imageset/Contents.json
vendored
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
10
phpmon/Assets.xcassets/LockUnlocked.imageset/Unlocked.svg
vendored
Normal 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 |
Before Width: | Height: | Size: 386 B After Width: | Height: | Size: 278 B |
Before Width: | Height: | Size: 780 B After Width: | Height: | Size: 500 B |
22
phpmon/Assets.xcassets/StatusBarIconStatic.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "phpmon.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "phpmon@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
phpmon/Assets.xcassets/StatusBarIconStatic.imageset/phpmon.png
vendored
Normal file
After Width: | Height: | Size: 229 B |
BIN
phpmon/Assets.xcassets/StatusBarIconStatic.imageset/phpmon@2x.png
vendored
Normal file
After Width: | Height: | Size: 353 B |
21
phpmon/Assets.xcassets/StatusBarPHP.imageset/Contents.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
BIN
phpmon/Assets.xcassets/StatusBarPHP.imageset/php@2x.png
vendored
Normal file
After Width: | Height: | Size: 940 B |
@ -9,11 +9,31 @@ import Cocoa
|
|||||||
|
|
||||||
class Constants {
|
class Constants {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The latest PHP version that is considered to be stable at the time of release.
|
||||||
|
* This version number is currently not used (only as a default fallback).
|
||||||
|
*/
|
||||||
|
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.
|
* The PHP versions supported by this application.
|
||||||
* Versions that do not appear in this array are omitted from the list.
|
* Versions that do not appear in this array are omitted from the list.
|
||||||
*/
|
*/
|
||||||
static let SupportedPhpVersions = [
|
static let SupportedPhpVersions = [
|
||||||
|
// ====================
|
||||||
|
// STABLE RELEASES
|
||||||
|
// ====================
|
||||||
|
// Versions of PHP that are stable and are supported.
|
||||||
"5.6",
|
"5.6",
|
||||||
"7.0",
|
"7.0",
|
||||||
"7.1",
|
"7.1",
|
||||||
@ -21,7 +41,14 @@ class Constants {
|
|||||||
"7.3",
|
"7.3",
|
||||||
"7.4",
|
"7.4",
|
||||||
"8.0",
|
"8.0",
|
||||||
"8.1"
|
"8.1",
|
||||||
|
|
||||||
|
// ====================
|
||||||
|
// EXPERIMENTAL SUPPORT
|
||||||
|
// ====================
|
||||||
|
// Every release that supports the next release will always support the next
|
||||||
|
// dev release. In this case, that means that the version below is detected.
|
||||||
|
"8.2"
|
||||||
]
|
]
|
||||||
|
|
||||||
}
|
}
|
||||||
|
22
phpmon/Credits.html
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: #FFF;
|
||||||
|
color: #000;
|
||||||
|
font-family: -apple-system;
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 5px;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<br>
|
||||||
|
<p><b>Want to spread the love?</b> Leave a <a href="https://github.com/nicoverbruggen/phpmon">star on GitHub</a>!</p>
|
||||||
|
<p><b>Having issues?</b> Consult the <a href="https://github.com/nicoverbruggen/phpmon#%EF%B8%8F-faq--troubleshooting">FAQ & Troubleshooting</a> section.</p>
|
||||||
|
<p><b>Want to support me?</b> You can <a href="https://nicoverbruggen.be/sponsor">financially support</a> the continued development of this app.</p>
|
||||||
|
<br>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
270
phpmon/Domain/Core/Actions.swift
Normal file
@ -0,0 +1,270 @@
|
|||||||
|
//
|
||||||
|
// Services.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import AppKit
|
||||||
|
|
||||||
|
class Actions {
|
||||||
|
|
||||||
|
// MARK: - Detect PHP Versions
|
||||||
|
|
||||||
|
public static func detectPhpVersions() -> [String]
|
||||||
|
{
|
||||||
|
let files = Shell.pipe("ls \(Paths.optPath) | grep php@")
|
||||||
|
var versionsOnly = Self.extractPhpVersions(from: files.components(separatedBy: "\n"))
|
||||||
|
|
||||||
|
// Make sure the aliased version is detected
|
||||||
|
// The user may have `php` installed, but not e.g. `php@8.0`
|
||||||
|
// We should also detect that as a version that is installed
|
||||||
|
let phpAlias = App.shared.brewPhpVersion
|
||||||
|
|
||||||
|
// Avoid inserting a duplicate
|
||||||
|
if (!versionsOnly.contains(phpAlias) && Shell.fileExists("\(Paths.optPath)/php/bin/php")) {
|
||||||
|
versionsOnly.append(phpAlias);
|
||||||
|
}
|
||||||
|
|
||||||
|
print("The PHP versions that were detected are: \(versionsOnly)")
|
||||||
|
|
||||||
|
App.shared.availablePhpVersions = versionsOnly
|
||||||
|
Actions.extractPhpLongVersions()
|
||||||
|
|
||||||
|
return versionsOnly
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
This method extracts the PHP full version number after finding the php installation folders.
|
||||||
|
To be refactored at some later point, I'd like to cache the `PhpInstallation` objects instead of just the version number at some point.
|
||||||
|
*/
|
||||||
|
public static func extractPhpLongVersions()
|
||||||
|
{
|
||||||
|
var mappedVersions: [String: PhpInstallation] = [:]
|
||||||
|
App.shared.availablePhpVersions.forEach { version in
|
||||||
|
mappedVersions[version] = PhpInstallation(version)
|
||||||
|
}
|
||||||
|
|
||||||
|
App.shared.cachedPhpInstallations = mappedVersions
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Extracts valid PHP versions from an array of strings.
|
||||||
|
This array of strings is usually retrieved from `grep`.
|
||||||
|
*/
|
||||||
|
public static func extractPhpVersions(
|
||||||
|
from versions: [String],
|
||||||
|
checkBinaries: Bool = true
|
||||||
|
) -> [String] {
|
||||||
|
var output : [String] = []
|
||||||
|
|
||||||
|
versions.filter { (version) -> Bool in
|
||||||
|
// Omit everything that doesn't start with php@
|
||||||
|
// (e.g. something-php@8.0 won't be detected)
|
||||||
|
return version.starts(with: "php@")
|
||||||
|
}.forEach { (string) in
|
||||||
|
let version = string.components(separatedBy: "php@")[1]
|
||||||
|
// Only append the version if it doesn't already exist (avoid dupes),
|
||||||
|
// is supported and where the binary exists (avoids broken installs)
|
||||||
|
if !output.contains(version)
|
||||||
|
&& Constants.SupportedPhpVersions.contains(version)
|
||||||
|
&& (checkBinaries ? Shell.fileExists("\(Paths.optPath)/php@\(version)/bin/php") : true)
|
||||||
|
{
|
||||||
|
output.append(version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Services
|
||||||
|
|
||||||
|
public static func restartPhpFpm()
|
||||||
|
{
|
||||||
|
brew("services restart \(App.phpInstall!.formula)", sudo: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func restartNginx()
|
||||||
|
{
|
||||||
|
brew("services restart nginx", sudo: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func restartDnsMasq()
|
||||||
|
{
|
||||||
|
brew("services restart dnsmasq", sudo: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func stopAllServices()
|
||||||
|
{
|
||||||
|
brew("services stop \(App.phpInstall!.formula)", sudo: true)
|
||||||
|
brew("services stop nginx", sudo: true)
|
||||||
|
brew("services stop dnsmasq", sudo: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Kindly asks Valet to switch to a specific PHP version.
|
||||||
|
*/
|
||||||
|
public static func switchToPhpVersionUsingValet(
|
||||||
|
version: String,
|
||||||
|
availableVersions: [String],
|
||||||
|
completed: @escaping () -> Void
|
||||||
|
) {
|
||||||
|
print("Switching to \(version) using Valet")
|
||||||
|
print(valet("use php@\(version)"))
|
||||||
|
completed()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Switching to a new PHP version involves:
|
||||||
|
- unlinking the current version
|
||||||
|
- stopping the active services
|
||||||
|
- linking the new desired version
|
||||||
|
|
||||||
|
Please note that depending on which version is installed,
|
||||||
|
the version that is switched to may or may not be identical to `php` (without @version).
|
||||||
|
*/
|
||||||
|
public static func switchToPhpVersion(
|
||||||
|
version: String,
|
||||||
|
availableVersions: [String],
|
||||||
|
completed: @escaping () -> Void
|
||||||
|
) {
|
||||||
|
print("Switching to \(version), unlinking all versions...")
|
||||||
|
|
||||||
|
let group = DispatchGroup()
|
||||||
|
|
||||||
|
availableVersions.forEach { (available) in
|
||||||
|
group.enter()
|
||||||
|
|
||||||
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
|
let formula = (available == App.shared.brewPhpVersion)
|
||||||
|
? "php" : "php@\(available)"
|
||||||
|
|
||||||
|
brew("unlink \(formula)")
|
||||||
|
brew("services stop \(formula)", sudo: true)
|
||||||
|
|
||||||
|
group.leave()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
group.notify(queue: .global(qos: .userInitiated)) {
|
||||||
|
print("All versions have been unlinked!")
|
||||||
|
print("Linking the new version!")
|
||||||
|
|
||||||
|
let formula = (version == App.shared.brewPhpVersion) ? "php" : "php@\(version)"
|
||||||
|
brew("link \(formula) --overwrite --force")
|
||||||
|
brew("services start \(formula)", sudo: true)
|
||||||
|
|
||||||
|
print("The new version has been linked!")
|
||||||
|
completed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Finding Config Files
|
||||||
|
|
||||||
|
public static func openGenericPhpConfigFolder()
|
||||||
|
{
|
||||||
|
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php")];
|
||||||
|
NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func openGlobalComposerFolder()
|
||||||
|
{
|
||||||
|
let file = FileManager.default.homeDirectoryForCurrentUser
|
||||||
|
.appendingPathComponent(".composer/composer.json")
|
||||||
|
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func openPhpConfigFolder(version: String)
|
||||||
|
{
|
||||||
|
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php/\(version)/php.ini")];
|
||||||
|
NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func openValetConfigFolder()
|
||||||
|
{
|
||||||
|
let file = FileManager.default.homeDirectoryForCurrentUser
|
||||||
|
.appendingPathComponent(".config/valet")
|
||||||
|
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Quick Fix
|
||||||
|
|
||||||
|
/**
|
||||||
|
Detects all currently available PHP versions,
|
||||||
|
and unlinks each and every one of them.
|
||||||
|
|
||||||
|
After this, the brew services are also stopped,
|
||||||
|
the latest PHP version is linked, and php + nginx are restarted.
|
||||||
|
|
||||||
|
If this does not solve the issue, the user may need to install additional
|
||||||
|
extensions and/or run `composer global update`.
|
||||||
|
*/
|
||||||
|
public static func fixMyPhp()
|
||||||
|
{
|
||||||
|
brew("services restart dnsmasq", sudo: true)
|
||||||
|
|
||||||
|
detectPhpVersions().forEach { (version) in
|
||||||
|
let formula = (version == App.shared.brewPhpVersion) ? "php" : "php@\(version)"
|
||||||
|
brew("unlink php@\(version)")
|
||||||
|
brew("services stop \(formula)")
|
||||||
|
brew("services stop \(formula)", sudo: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
brew("services stop php")
|
||||||
|
brew("services stop nginx")
|
||||||
|
brew("link php")
|
||||||
|
brew("services restart dnsmasq", sudo: true)
|
||||||
|
brew("services stop php", sudo: true)
|
||||||
|
brew("services stop nginx", sudo: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Common Shell Commands
|
||||||
|
|
||||||
|
/**
|
||||||
|
Runs a `valet` command.
|
||||||
|
*/
|
||||||
|
public static func valet(_ command: String) -> String
|
||||||
|
{
|
||||||
|
return Shell.pipe("sudo \(Paths.valet) \(command)", requiresPath: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Runs a `brew` command. Can run as superuser.
|
||||||
|
*/
|
||||||
|
public static func brew(_ command: String, sudo: Bool = false)
|
||||||
|
{
|
||||||
|
Shell.run("\(sudo ? "sudo " : "")" + "\(Paths.brew) \(command)")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Runs `sed` in order to replace all occurrences of a string in a specific file with another.
|
||||||
|
*/
|
||||||
|
public static func sed(file: String, original: String, replacement: String)
|
||||||
|
{
|
||||||
|
// Escape slashes (or `sed` won't work)
|
||||||
|
let e_original = original.replacingOccurrences(of: "/", with: "\\/")
|
||||||
|
let e_replacement = replacement.replacingOccurrences(of: "/", with: "\\/")
|
||||||
|
|
||||||
|
// Check if gsed exists; it is able to follow symlinks,
|
||||||
|
// which we want to do to toggle the extension
|
||||||
|
if Shell.fileExists("\(Paths.binPath)/gsed") {
|
||||||
|
Shell.run("\(Paths.binPath)/gsed -i --follow-symlinks 's/\(e_original)/\(e_replacement)/g' \(file)")
|
||||||
|
} else {
|
||||||
|
Shell.run("sed -i '' 's/\(e_original)/\(e_replacement)/g' \(file)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Uses `grep` to determine whether a particular query string can be found in a particular file.
|
||||||
|
*/
|
||||||
|
public static func grepContains(file: String, query: String) -> Bool
|
||||||
|
{
|
||||||
|
return Shell.pipe("""
|
||||||
|
grep -q '\(query)' \(file); [ $? -eq 0 ] && echo "YES" || echo "NO"
|
||||||
|
""")
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
.contains("YES")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
44
phpmon/Domain/Core/App+ActivationPolicy.swift
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
55
phpmon/Domain/Core/App+GlobalHotkey.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -4,46 +4,67 @@
|
|||||||
//
|
//
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
import HotKey
|
||||||
|
|
||||||
class App {
|
class App {
|
||||||
|
|
||||||
|
// MARK: Static Vars
|
||||||
|
|
||||||
|
/** The static app instance. Accessible at any time. */
|
||||||
static let shared = App()
|
static let shared = App()
|
||||||
|
|
||||||
static var phpInstall: PhpInstallation? {
|
/** 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. */
|
||||||
|
static var phpInstall: ActivePhpInstallation? {
|
||||||
return App.shared.currentInstall
|
return App.shared.currentInstall
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whether the app is busy doing something. Used to determine what UI to display. */
|
||||||
static var busy: Bool {
|
static var busy: Bool {
|
||||||
return App.shared.busy
|
return App.shared.busy
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// MARK: Variables
|
||||||
Whether the application is busy switching versions.
|
|
||||||
*/
|
/** The list of preferences that are currently active. */
|
||||||
|
var preferences: [PreferenceName: Bool]!
|
||||||
|
|
||||||
|
/** The window controller of the currently active preferences window. */
|
||||||
|
var preferencesWindowController: PrefsWC? = nil
|
||||||
|
|
||||||
|
/** 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
|
var busy: Bool = false
|
||||||
|
|
||||||
/**
|
/** The currently active installation of PHP. */
|
||||||
The currently active installation of PHP.
|
var currentInstall: ActivePhpInstallation? = nil
|
||||||
*/
|
|
||||||
var currentInstall: PhpInstallation? = nil
|
|
||||||
|
|
||||||
/**
|
/** All available versions of PHP. */
|
||||||
All available versions of PHP.
|
var availablePhpVersions: [String] = []
|
||||||
*/
|
|
||||||
var availablePhpVersions : [String] = []
|
|
||||||
|
|
||||||
/**
|
/** Cached information about the PHP installations. */
|
||||||
The timer that will periodically fetch the PHP version that is currently active.
|
var cachedPhpInstallations: [String: PhpInstallation] = [:]
|
||||||
*/
|
|
||||||
|
/** List of detected (installed) applications that PHP Monitor can work with. */
|
||||||
|
var detectedApplications: [Application] = []
|
||||||
|
|
||||||
|
/** Timer that will periodically reload info about the user's PHP installation. */
|
||||||
var timer: Timer?
|
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 {
|
||||||
*/
|
|
||||||
var brewPhpPackage: HomebrewPackage? = nil {
|
|
||||||
didSet {
|
didSet {
|
||||||
self.brewPhpVersion = self.brewPhpPackage!.version
|
brewPhpVersion = brewPhpPackage!.version
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,9 +74,32 @@ class App {
|
|||||||
If you're up to date, `php` will be aliased to the latest version,
|
If you're up to date, `php` will be aliased to the latest version,
|
||||||
but that might not be the case.
|
but that might not be the case.
|
||||||
|
|
||||||
We'll technically default to version 8.0, but the information should always be loaded
|
We'll technically default to the version in Constants.swift, but the information
|
||||||
from Homebrew itself upon starting the application.
|
should always be loaded from Homebrew itself upon startup.
|
||||||
*/
|
*/
|
||||||
var brewPhpVersion: String = "8.0"
|
var brewPhpVersion: String = Constants.LatestStablePhpVersion
|
||||||
|
|
||||||
|
// MARK: - Global Hotkey
|
||||||
|
|
||||||
|
/**
|
||||||
|
The shortcut the user has requested.
|
||||||
|
*/
|
||||||
|
var shortcutHotkey: HotKey? = nil {
|
||||||
|
didSet {
|
||||||
|
setupGlobalHotkeyListener()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Activation Policy
|
||||||
|
|
||||||
|
/**
|
||||||
|
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).
|
||||||
|
*/
|
||||||
|
var openWindows: [String] = []
|
||||||
|
|
||||||
}
|
}
|
||||||
|
40
phpmon/Domain/Core/AppDelegate+MenuOutlets.swift
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
42
phpmon/Domain/Core/AppDelegate+Notifications.swift
Normal 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])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -9,7 +9,7 @@ import Cocoa
|
|||||||
import UserNotifications
|
import UserNotifications
|
||||||
|
|
||||||
@NSApplicationMain
|
@NSApplicationMain
|
||||||
class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDelegate {
|
class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate {
|
||||||
|
|
||||||
// MARK: - Variables
|
// MARK: - Variables
|
||||||
|
|
||||||
@ -18,25 +18,31 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele
|
|||||||
(invoked by PHP Monitor) shell commands. It is used to
|
(invoked by PHP Monitor) shell commands. It is used to
|
||||||
invoke all commands in this application.
|
invoke all commands in this application.
|
||||||
*/
|
*/
|
||||||
let sharedShell : Shell
|
let sharedShell: Shell
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The App singleton contains information about the state of
|
The App singleton contains information about the state of
|
||||||
the application and global variables.
|
the application and global variables.
|
||||||
*/
|
*/
|
||||||
let state : App
|
let state: App
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The MainMenu singleton is responsible for rendering the
|
The MainMenu singleton is responsible for rendering the
|
||||||
menu bar item and its menu, as well as its actions.
|
menu bar item and its menu, as well as its actions.
|
||||||
*/
|
*/
|
||||||
let menu : MainMenu
|
let menu: MainMenu
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The paths singleton that determines where Homebrew is installed,
|
The paths singleton that determines where Homebrew is installed,
|
||||||
and where to look for binaries.
|
and where to look for binaries.
|
||||||
*/
|
*/
|
||||||
let paths : Paths
|
let paths: Paths
|
||||||
|
|
||||||
|
/**
|
||||||
|
The Valet singleton that determines all information
|
||||||
|
about Valet and its current configuration.
|
||||||
|
*/
|
||||||
|
let valet: Valet
|
||||||
|
|
||||||
// MARK: - Initializer
|
// MARK: - Initializer
|
||||||
|
|
||||||
@ -44,10 +50,15 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele
|
|||||||
When the application initializes, create all singletons.
|
When the application initializes, create all singletons.
|
||||||
*/
|
*/
|
||||||
override init() {
|
override init() {
|
||||||
|
print("==================================")
|
||||||
|
print("PHP MONITOR by Nico Verbruggen")
|
||||||
|
print("Version \(App.version)")
|
||||||
|
print("==================================")
|
||||||
self.sharedShell = Shell.user
|
self.sharedShell = Shell.user
|
||||||
self.state = App.shared
|
self.state = App.shared
|
||||||
self.menu = MainMenu.shared
|
self.menu = MainMenu.shared
|
||||||
self.paths = Paths.shared
|
self.paths = Paths.shared
|
||||||
|
self.valet = Valet.shared
|
||||||
super.init()
|
super.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,26 +66,14 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
When the application has finished launching, we'll want to set up
|
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.
|
startup procedure.
|
||||||
*/
|
*/
|
||||||
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||||
NSUserNotificationCenter.default.delegate = self
|
// Make sure notifications will work
|
||||||
self.menu.startup()
|
setupNotifications()
|
||||||
|
// Make sure the menu performs its initial checks
|
||||||
|
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
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,8 +1,11 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="17506" 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>
|
<dependencies>
|
||||||
<deployment identifier="macosx"/>
|
<deployment identifier="macosx"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="17506"/>
|
<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>
|
</dependencies>
|
||||||
<scenes>
|
<scenes>
|
||||||
<!--Application-->
|
<!--Application-->
|
||||||
@ -30,6 +33,249 @@
|
|||||||
</items>
|
</items>
|
||||||
</menu>
|
</menu>
|
||||||
</menuItem>
|
</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">
|
<menuItem title="Help" id="wpr-3q-Mcd">
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
<menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ">
|
<menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ">
|
||||||
@ -48,11 +294,318 @@
|
|||||||
<outlet property="delegate" destination="Voe-Tx-rLC" id="PrD-fu-P6m"/>
|
<outlet property="delegate" destination="Voe-Tx-rLC" id="PrD-fu-P6m"/>
|
||||||
</connections>
|
</connections>
|
||||||
</application>
|
</application>
|
||||||
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="PHP_Monitor" customModuleProvider="target"/>
|
|
||||||
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
|
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
|
||||||
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
<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>
|
</objects>
|
||||||
<point key="canvasLocation" x="-343" y="-16"/>
|
<point key="canvasLocation" x="-484" y="32"/>
|
||||||
|
</scene>
|
||||||
|
<!--Window Controller-->
|
||||||
|
<scene sceneID="PQa-AT-b2a">
|
||||||
|
<objects>
|
||||||
|
<windowController storyboardIdentifier="preferencesWindow" showSeguePresentationStyle="single" id="hLJ-Fd-wRr" customClass="PrefsWC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
|
<window key="window" title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="h4c-3b-nko">
|
||||||
|
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
||||||
|
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||||
|
<rect key="contentRect" x="372" y="403" width="480" height="270"/>
|
||||||
|
<rect key="screenRect" x="0.0" y="0.0" width="2304" height="1271"/>
|
||||||
|
<view key="contentView" id="2yL-50-11x">
|
||||||
|
<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>
|
||||||
|
</window>
|
||||||
|
<connections>
|
||||||
|
<segue destination="AW2-rV-rbS" kind="relationship" relationship="window.shadowedContentViewController" id="3dX-9V-eA0"/>
|
||||||
|
</connections>
|
||||||
|
</windowController>
|
||||||
|
<customObject id="OF0-qs-3Oh" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="-374" y="327"/>
|
||||||
|
</scene>
|
||||||
|
<!--Preferences-->
|
||||||
|
<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" id="Pf1-A5-3Xz">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="574" height="498"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<subviews>
|
||||||
|
<stackView distribution="fillEqually" orientation="vertical" alignment="leading" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="k57-O3-Yyj">
|
||||||
|
<rect key="frame" x="0.0" y="15" width="574" height="468"/>
|
||||||
|
</stackView>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<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="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="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 & 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>
|
||||||
|
<button translatesAutoresizingMaskIntoConstraints="NO" id="ypa-iv-wLD">
|
||||||
|
<rect key="frame" x="211" y="18" width="18" height="18"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="width" constant="18" id="jKJ-Xn-BPA"/>
|
||||||
|
<constraint firstAttribute="height" constant="18" id="lSH-of-WzD"/>
|
||||||
|
</constraints>
|
||||||
|
<buttonCell key="cell" type="square" bezelStyle="shadowlessSquare" image="NSCaution" imagePosition="only" alignment="center" imageScaling="proportionallyUpOrDown" inset="2" id="9XB-KO-aSI">
|
||||||
|
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||||
|
<font key="font" metaFont="system"/>
|
||||||
|
</buttonCell>
|
||||||
|
</button>
|
||||||
|
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="150" translatesAutoresizingMaskIntoConstraints="NO" id="MD8-ef-Ht8">
|
||||||
|
<rect key="frame" x="235" y="16" width="182" height="22"/>
|
||||||
|
<textFieldCell key="cell" sendsActionOnEndEditing="YES" title="Warning: This is a warning message. Please take this into account." id="iub-KH-clf">
|
||||||
|
<font key="font" metaFont="system" size="9"/>
|
||||||
|
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
</textFieldCell>
|
||||||
|
</textField>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="0NQ-ZD-CqD" firstAttribute="leading" secondItem="MD8-ef-Ht8" secondAttribute="trailing" constant="20" id="1Rb-Or-Nnn"/>
|
||||||
|
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="TbX-e2-3QL" secondAttribute="trailing" constant="20" symbolic="YES" id="3vE-LR-S7N"/>
|
||||||
|
<constraint firstItem="TbX-e2-3QL" firstAttribute="leading" secondItem="0NQ-ZD-CqD" secondAttribute="trailing" constant="8" symbolic="YES" id="4cb-D9-8d1"/>
|
||||||
|
<constraint firstItem="XJL-Uw-frD" firstAttribute="leading" secondItem="QPX-eu-eV8" secondAttribute="trailing" constant="10" id="55y-3V-RYt"/>
|
||||||
|
<constraint firstItem="syz-LF-l6P" firstAttribute="leading" secondItem="5GY-nN-BWd" secondAttribute="leading" id="8QK-nf-Fiw"/>
|
||||||
|
<constraint firstItem="QPX-eu-eV8" firstAttribute="top" secondItem="XJL-Uw-frD" secondAttribute="top" id="9QB-jZ-k1V"/>
|
||||||
|
<constraint firstItem="ypa-iv-wLD" firstAttribute="centerY" secondItem="5GY-nN-BWd" secondAttribute="centerY" id="9d8-P2-iSk"/>
|
||||||
|
<constraint firstItem="MD8-ef-Ht8" firstAttribute="leading" secondItem="ypa-iv-wLD" secondAttribute="trailing" constant="8" symbolic="YES" id="C90-wQ-3Gf"/>
|
||||||
|
<constraint firstItem="QPX-eu-eV8" firstAttribute="leading" secondItem="5GY-nN-BWd" secondAttribute="leading" constant="10" id="GOj-sw-ZlZ"/>
|
||||||
|
<constraint firstItem="TbX-e2-3QL" firstAttribute="top" secondItem="jKi-Ls-7FZ" secondAttribute="bottom" constant="-1" id="J29-wT-Uex"/>
|
||||||
|
<constraint firstItem="CXK-Q9-CpO" firstAttribute="leading" secondItem="XJL-Uw-frD" secondAttribute="leading" id="Ojw-VZ-3EG"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="syz-LF-l6P" secondAttribute="trailing" id="PWd-5k-AlD"/>
|
||||||
|
<constraint firstItem="XJL-Uw-frD" firstAttribute="top" secondItem="5GY-nN-BWd" secondAttribute="top" constant="12" id="QeE-c7-I9U"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="jKi-Ls-7FZ" secondAttribute="trailing" constant="10" id="Uhk-Dy-c65"/>
|
||||||
|
<constraint firstItem="0NQ-ZD-CqD" firstAttribute="centerY" secondItem="5GY-nN-BWd" secondAttribute="centerY" id="Utr-aa-tqX"/>
|
||||||
|
<constraint firstItem="CXK-Q9-CpO" firstAttribute="top" secondItem="XJL-Uw-frD" secondAttribute="bottom" id="VKg-Vq-sYa"/>
|
||||||
|
<constraint firstItem="TbX-e2-3QL" firstAttribute="centerY" secondItem="5GY-nN-BWd" secondAttribute="centerY" constant="5" id="cN8-zO-fnc"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="syz-LF-l6P" secondAttribute="bottom" id="gj7-cJ-Lle"/>
|
||||||
|
<constraint firstItem="0NQ-ZD-CqD" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="CXK-Q9-CpO" secondAttribute="trailing" constant="8" symbolic="YES" id="iEd-Y3-zhp"/>
|
||||||
|
<constraint firstItem="ypa-iv-wLD" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="XJL-Uw-frD" secondAttribute="trailing" constant="30" id="koV-Sj-tO8"/>
|
||||||
|
<constraint firstItem="MD8-ef-Ht8" firstAttribute="centerY" secondItem="ypa-iv-wLD" secondAttribute="centerY" id="lIN-pm-mCo"/>
|
||||||
|
<constraint firstItem="0NQ-ZD-CqD" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="XJL-Uw-frD" secondAttribute="trailing" constant="8" symbolic="YES" id="lLA-Jx-Q4W"/>
|
||||||
|
<constraint firstItem="jKi-Ls-7FZ" firstAttribute="leading" secondItem="TbX-e2-3QL" secondAttribute="leading" id="zjN-s3-2Ww"/>
|
||||||
|
</constraints>
|
||||||
|
<connections>
|
||||||
|
<outlet property="buttonWarning" destination="ypa-iv-wLD" id="NwX-H3-8um"/>
|
||||||
|
<outlet property="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"/>
|
||||||
|
<outlet property="labelWarning" destination="MD8-ef-Ht8" id="Faw-CY-9R5"/>
|
||||||
|
</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="764.5"/>
|
||||||
</scene>
|
</scene>
|
||||||
</scenes>
|
</scenes>
|
||||||
|
<resources>
|
||||||
|
<image name="IconLinked" width="512" height="512"/>
|
||||||
|
<image name="Lock" width="30" height="30"/>
|
||||||
|
<image name="NSCaution" width="32" height="32"/>
|
||||||
|
<image name="arrow.clockwise" catalog="system" width="14" height="16"/>
|
||||||
|
</resources>
|
||||||
</document>
|
</document>
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
//
|
|
||||||
// HomebrewPackage.swift
|
|
||||||
// PHP Monitor
|
|
||||||
//
|
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
struct HomebrewPackage : Decodable {
|
|
||||||
let name: String
|
|
||||||
let full_name: String
|
|
||||||
let aliases: [String]
|
|
||||||
|
|
||||||
public var version: String {
|
|
||||||
return aliases.first!.replacingOccurrences(of: "php@", with: "")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,114 +0,0 @@
|
|||||||
//
|
|
||||||
// PhpInstallation.swift
|
|
||||||
// PHP Monitor
|
|
||||||
//
|
|
||||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
class PhpInstallation {
|
|
||||||
|
|
||||||
var version: Version
|
|
||||||
var configuration: Configuration
|
|
||||||
var extensions: [PhpExtension]
|
|
||||||
|
|
||||||
// MARK: - Computed
|
|
||||||
|
|
||||||
var formula: String {
|
|
||||||
return (self.version.short == App.shared.brewPhpVersion) ? "php" : "php@\(self.version.short)"
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Initializer
|
|
||||||
|
|
||||||
init() {
|
|
||||||
// Show information about the current version
|
|
||||||
self.version = Self.getVersion()
|
|
||||||
|
|
||||||
// If an error occurred, exit early
|
|
||||||
if (self.version.error) {
|
|
||||||
self.configuration = Configuration()
|
|
||||||
self.extensions = []
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load extension information
|
|
||||||
let path = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(self.version.short)/php.ini")
|
|
||||||
self.extensions = PhpExtension.load(from: path)
|
|
||||||
|
|
||||||
// Get configuration values
|
|
||||||
self.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")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
When the app tries to retrieve the version, the installation is considered broken if the output is nothing,
|
|
||||||
_or_ if the output contains the word "Warning" or "Error". In normal situations this should not be the case.
|
|
||||||
*/
|
|
||||||
private static func getVersion() -> Version {
|
|
||||||
var versionStruct = Version()
|
|
||||||
let version = Command.execute(path: Paths.php, arguments: ["-r", "print phpversion();"])
|
|
||||||
|
|
||||||
if (version == "" || version.contains("Warning") || version.contains("Error")) {
|
|
||||||
versionStruct.short = "💩 BROKEN"
|
|
||||||
versionStruct.long = "";
|
|
||||||
versionStruct.error = true
|
|
||||||
return versionStruct;
|
|
||||||
}
|
|
||||||
|
|
||||||
// That's the long version
|
|
||||||
versionStruct.long = version
|
|
||||||
|
|
||||||
// Next up, let's strip away the minor version number
|
|
||||||
let segments = versionStruct.long.components(separatedBy: ".")
|
|
||||||
|
|
||||||
// Get the first two elements
|
|
||||||
versionStruct.short = segments[0...1].joined(separator: ".")
|
|
||||||
|
|
||||||
return versionStruct
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Retrieves the display value for a specific key in the `.ini` file.
|
|
||||||
|
|
||||||
The following values are valid:
|
|
||||||
* -1: unlimited (show the infinity icon)
|
|
||||||
* 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 ⚠️.
|
|
||||||
|
|
||||||
- Parameter key: The key of the `ini` value that needs to be retrieved. For example, you can use `memory_limit`.
|
|
||||||
*/
|
|
||||||
private static func getByteCount(key: String) -> String {
|
|
||||||
let value = Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('\(key)');"])
|
|
||||||
|
|
||||||
// Check if the value is unlimited
|
|
||||||
if (value == "-1") {
|
|
||||||
return "∞"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the syntax is valid otherwise
|
|
||||||
let regex = try! NSRegularExpression(pattern: #"^([0-9]*)(K|M|G|)$"#, options: [])
|
|
||||||
let match = regex.matches(in: value, options: [], range: NSMakeRange(0, value.count)).first
|
|
||||||
return (match == nil) ? "⚠️" : "\(value)B"
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Structs
|
|
||||||
|
|
||||||
struct Version {
|
|
||||||
var short = "???"
|
|
||||||
var long = "???"
|
|
||||||
var error = false
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Configuration {
|
|
||||||
var memory_limit = "???"
|
|
||||||
var upload_max_filesize = "???"
|
|
||||||
var post_max_size = "???"
|
|
||||||
}
|
|
||||||
}
|
|
@ -21,57 +21,60 @@ class Startup {
|
|||||||
*/
|
*/
|
||||||
func checkEnvironment(success: () -> Void, failure: @escaping () -> Void)
|
func checkEnvironment(success: () -> Void, failure: @escaping () -> Void)
|
||||||
{
|
{
|
||||||
self.failureCallback = failure
|
failureCallback = failure
|
||||||
|
|
||||||
self.performEnvironmentCheck(
|
performEnvironmentCheck(
|
||||||
!Shell.fileExists("\(Paths.binPath)/php"),
|
!Shell.fileExists("\(Paths.binPath)/php"),
|
||||||
messageText: "startup.errors.php_binary.title".localized,
|
messageText: "startup.errors.php_binary.title".localized,
|
||||||
informativeText: "startup.errors.php_binary_desc".localized,
|
informativeText: "startup.errors.php_binary.desc".localized,
|
||||||
breaking: true
|
breaking: true
|
||||||
)
|
)
|
||||||
|
|
||||||
self.performEnvironmentCheck(
|
performEnvironmentCheck(
|
||||||
!Shell.pipe("ls \(Paths.optPath) | grep php").contains("php"),
|
!Shell.pipe("ls \(Paths.optPath) | grep php").contains("php"),
|
||||||
messageText: "startup.errors.php_opt.title".localized,
|
messageText: "startup.errors.php_opt.title".localized,
|
||||||
informativeText: "startup.errors.php_opt.desc".localized,
|
informativeText: "startup.errors.php_opt.desc".localized,
|
||||||
breaking: true
|
breaking: true
|
||||||
)
|
)
|
||||||
|
|
||||||
self.performEnvironmentCheck(
|
performEnvironmentCheck(
|
||||||
// Older versions of Valet might be located in `/usr/local/bin` regardless of Homebrew prefix
|
// Check for Valet; it can be symlinked or in .composer/vendor/bin
|
||||||
!(Shell.pipe("which valet").contains("/usr/local/bin/valet")
|
!(Shell.fileExists("/usr/local/bin/valet")
|
||||||
|| Shell.pipe("which valet").contains("/opt/homebrew/bin/valet")),
|
|| Shell.fileExists("/opt/homebrew/bin/valet")
|
||||||
|
|| Shell.fileExists("~/.composer/vendor/bin/valet")
|
||||||
|
),
|
||||||
messageText: "startup.errors.valet_executable.title".localized,
|
messageText: "startup.errors.valet_executable.title".localized,
|
||||||
informativeText: "startup.errors.valet_executable.desc".localized,
|
informativeText: "startup.errors.valet_executable.desc".localized,
|
||||||
breaking: true
|
breaking: true
|
||||||
)
|
)
|
||||||
|
|
||||||
self.performEnvironmentCheck(
|
performEnvironmentCheck(
|
||||||
!Shell.pipe("cat /private/etc/sudoers.d/brew").contains("\(Paths.binPath)/brew"),
|
!Shell.pipe("cat /private/etc/sudoers.d/brew").contains("\(Paths.binPath)/brew"),
|
||||||
messageText: "startup.errors.sudoers_brew.title".localized,
|
messageText: "startup.errors.sudoers_brew.title".localized,
|
||||||
informativeText: "startup.errors.sudoers_brew.desc".localized,
|
informativeText: "startup.errors.sudoers_brew.desc".localized,
|
||||||
breaking: true
|
breaking: true
|
||||||
)
|
)
|
||||||
|
|
||||||
self.performEnvironmentCheck(
|
performEnvironmentCheck(
|
||||||
// Older versions of Valet might be located in `/usr/local/bin` regardless of Homebrew prefix
|
// Check for Valet; it can be symlinked or in .composer/vendor/bin
|
||||||
!(Shell.pipe("cat /private/etc/sudoers.d/valet").contains("/usr/local/bin/valet")
|
!(Shell.pipe("cat /private/etc/sudoers.d/valet").contains("/usr/local/bin/valet")
|
||||||
|| Shell.pipe("cat /private/etc/sudoers.d/valet").contains("/opt/homebrew/bin/valet")),
|
|| Shell.pipe("cat /private/etc/sudoers.d/valet").contains("/opt/homebrew/bin/valet")
|
||||||
|
),
|
||||||
messageText: "startup.errors.sudoers_valet.title".localized,
|
messageText: "startup.errors.sudoers_valet.title".localized,
|
||||||
informativeText: "startup.errors.sudoers_valet.desc".localized,
|
informativeText: "startup.errors.sudoers_valet.desc".localized,
|
||||||
breaking: true
|
breaking: true
|
||||||
)
|
)
|
||||||
|
|
||||||
let services = Shell.pipe("\(Paths.brew) services list | grep php")
|
let services = Shell.pipe("\(Paths.brew) services list | grep php")
|
||||||
self.performEnvironmentCheck(
|
performEnvironmentCheck(
|
||||||
(services.countInstances(of: "started") > 1),
|
(services.countInstances(of: "started") > 1),
|
||||||
messageText: "startup.errors.services.title".localized,
|
messageText: "startup.errors.services.title".localized,
|
||||||
informativeText: "startup.errors.services.desc".localized,
|
informativeText: "startup.errors.services.desc".localized,
|
||||||
breaking: false
|
breaking: false
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!self.failed) {
|
if (!failed) {
|
||||||
self.determineBrewAliasVersion()
|
determineBrewAliasVersion()
|
||||||
success()
|
success()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -111,13 +114,18 @@ class Startup {
|
|||||||
) {
|
) {
|
||||||
if (!condition) { return }
|
if (!condition) { return }
|
||||||
|
|
||||||
self.failed = breaking
|
failed = breaking
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async { [self] in
|
||||||
// Present the information to the user
|
// Present the information to the user
|
||||||
Alert.notify(message: messageText, info: informativeText)
|
Alert.notify(
|
||||||
|
message: messageText,
|
||||||
|
info: informativeText,
|
||||||
|
style: breaking ? .critical : .warning
|
||||||
|
)
|
||||||
// Only breaking issues will throw the extra retry modal
|
// Only breaking issues will throw the extra retry modal
|
||||||
breaking ? self.failureCallback() : ()
|
breaking ? failureCallback() : ()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -7,11 +7,12 @@
|
|||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
extension Date
|
extension Date {
|
||||||
{
|
|
||||||
func toString() -> String {
|
func toString() -> String {
|
||||||
let dateFormatter = DateFormatter()
|
let dateFormatter = DateFormatter()
|
||||||
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||||
return dateFormatter.string(from: self)
|
return dateFormatter.string(from: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
18
phpmon/Domain/Extensions/NSMenuExtension.swift
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
//
|
||||||
|
// NSMenuExtension.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 14/04/2021.
|
||||||
|
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Cocoa
|
||||||
|
|
||||||
|
extension NSMenu {
|
||||||
|
|
||||||
|
open func addItem(_ newItem: NSMenuItem, withKeyModifier modifier: NSEvent.ModifierFlags) {
|
||||||
|
newItem.keyEquivalentModifierMask = modifier
|
||||||
|
self.addItem(newItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -12,6 +12,10 @@ extension String {
|
|||||||
return NSLocalizedString(self, tableName: nil, bundle: Bundle.main, value: "", comment: "")
|
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 {
|
func countInstances(of stringToFind: String) -> Int {
|
||||||
if (stringToFind.isEmpty) {
|
if (stringToFind.isEmpty) {
|
||||||
return 0
|
return 0
|
||||||
@ -33,4 +37,38 @@ extension String {
|
|||||||
let end = r.upperBound
|
let end = r.upperBound
|
||||||
return String(self[start ..< end])
|
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>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -7,17 +7,19 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
// Adapted from: https://stackoverflow.com/a/46268778
|
// Adapted from: https://stackoverflow.com/a/46268778
|
||||||
|
|
||||||
protocol XibLoadable {
|
protocol XibLoadable {
|
||||||
|
|
||||||
static var xibName: String? { get }
|
static var xibName: String? { get }
|
||||||
static func createFromXib(in bundle: Bundle) -> Self?
|
static func createFromXib(in bundle: Bundle) -> Self?
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension XibLoadable where Self: NSView {
|
extension XibLoadable where Self: NSView {
|
||||||
|
|
||||||
static var xibName: String? {
|
static var xibName: String? {
|
||||||
return String(describing: Self.self)
|
return String(describing: Self.self)
|
||||||
}
|
}
|
||||||
@ -30,4 +32,5 @@ extension XibLoadable where Self: NSView {
|
|||||||
let views = Array<Any>(results).filter { $0 is Self }
|
let views = Array<Any>(results).filter { $0 is Self }
|
||||||
return views.last as? Self
|
return views.last as? Self
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -13,9 +13,11 @@ class Alert {
|
|||||||
messageText: String,
|
messageText: String,
|
||||||
informativeText: String,
|
informativeText: String,
|
||||||
buttonTitle: String = "OK",
|
buttonTitle: String = "OK",
|
||||||
secondButtonTitle: String = ""
|
secondButtonTitle: String = "",
|
||||||
|
style: NSAlert.Style = .informational
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
let alert = NSAlert.init()
|
let alert = NSAlert.init()
|
||||||
|
alert.alertStyle = style
|
||||||
alert.messageText = messageText
|
alert.messageText = messageText
|
||||||
alert.informativeText = informativeText
|
alert.informativeText = informativeText
|
||||||
alert.addButton(withTitle: buttonTitle)
|
alert.addButton(withTitle: buttonTitle)
|
||||||
@ -25,8 +27,38 @@ class Alert {
|
|||||||
return alert.runModal() == .alertFirstButtonReturn
|
return alert.runModal() == .alertFirstButtonReturn
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func notify(message: String, info: String) {
|
public static func confirm(
|
||||||
_ = self.present(messageText: message, informativeText: info, buttonTitle: "OK", secondButtonTitle: "")
|
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) {
|
||||||
|
_ = present(
|
||||||
|
messageText: message,
|
||||||
|
informativeText: info,
|
||||||
|
buttonTitle: "OK",
|
||||||
|
secondButtonTitle: "",
|
||||||
|
style: style
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
64
phpmon/Domain/Helpers/Application.swift
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
phpmon/Domain/Helpers/Filesystem.swift
Normal 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)")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -6,14 +6,28 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import UserNotifications
|
||||||
|
|
||||||
class LocalNotification {
|
class LocalNotification {
|
||||||
|
|
||||||
public static func send(title: String, subtitle: String) {
|
public static func send(title: String, subtitle: String) {
|
||||||
let notification = NSUserNotification()
|
let content = UNMutableNotificationContent()
|
||||||
notification.title = title
|
content.title = title
|
||||||
notification.subtitle = subtitle
|
content.body = subtitle
|
||||||
NSUserNotificationCenter.default.deliver(notification)
|
|
||||||
|
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!)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -41,6 +41,7 @@ class MenuBarImageGenerator {
|
|||||||
let textRect = CGRect(x: padding, y: 0.5, width: image.size.width, height: image.size.height)
|
let textRect = CGRect(x: padding, y: 0.5, width: image.size.width, height: image.size.height)
|
||||||
|
|
||||||
let targetImage: NSImage = NSImage(size: image.size)
|
let targetImage: NSImage = NSImage(size: image.size)
|
||||||
|
|
||||||
let rep: NSBitmapImageRep = NSBitmapImageRep(
|
let rep: NSBitmapImageRep = NSBitmapImageRep(
|
||||||
bitmapDataPlanes: nil,
|
bitmapDataPlanes: nil,
|
||||||
pixelsWide: Int(image.size.width),
|
pixelsWide: Int(image.size.width),
|
||||||
@ -56,7 +57,7 @@ class MenuBarImageGenerator {
|
|||||||
|
|
||||||
targetImage.addRepresentation(rep)
|
targetImage.addRepresentation(rep)
|
||||||
targetImage.lockFocus()
|
targetImage.lockFocus()
|
||||||
|
|
||||||
image.draw(in: imageRect)
|
image.draw(in: imageRect)
|
||||||
text.draw(in: textRect, withAttributes: textFontAttributes)
|
text.draw(in: textRect, withAttributes: textFontAttributes)
|
||||||
|
|
||||||
@ -64,4 +65,34 @@ class MenuBarImageGenerator {
|
|||||||
return targetImage
|
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
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
48
phpmon/Domain/Helpers/PMWindowController.swift
Normal 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
32
phpmon/Domain/Helpers/Timer.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
37
phpmon/Domain/Helpers/VersionExtractor.swift
Normal 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])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
//
|
||||||
|
// AliasConflict.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 28/11/2021.
|
||||||
|
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class HomebrewDiagnostics {
|
||||||
|
|
||||||
|
enum Errors: String {
|
||||||
|
case aliasConflict = "alias_conflict"
|
||||||
|
}
|
||||||
|
|
||||||
|
static let shared = HomebrewDiagnostics()
|
||||||
|
var errors: [HomebrewDiagnostics.Errors] = []
|
||||||
|
|
||||||
|
init() {
|
||||||
|
if determineAliasConflicts() {
|
||||||
|
errors.append(.aliasConflict)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
It is possible to have the `shivammathur/php` tap installed, and for the core homebrew information to be outdated.
|
||||||
|
This will then result in two different aliases claiming to point to the same formula (`php`).
|
||||||
|
This will break all linking functionality in PHP Monitor, and the user needs to be informed of this.
|
||||||
|
|
||||||
|
This check only needs to be performed if the `shivammathur/php` tap is active.
|
||||||
|
*/
|
||||||
|
public func determineAliasConflicts() -> Bool
|
||||||
|
{
|
||||||
|
let tapAlias = Shell.pipe("\(Paths.brew) info shivammathur/php/php --json")
|
||||||
|
|
||||||
|
if tapAlias.contains("brew tap shivammathur/php") || tapAlias.contains("Error") {
|
||||||
|
print("The user does not appear to have tapped: shivammathur/php")
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
print("The user DOES have the following tapped: shivammathur/php")
|
||||||
|
print("Checking for `php` formula conflicts...")
|
||||||
|
|
||||||
|
let tapPhp = try! JSONDecoder().decode(
|
||||||
|
[HomebrewPackage].self,
|
||||||
|
from: tapAlias.data(using: .utf8)!
|
||||||
|
).first!
|
||||||
|
|
||||||
|
if tapPhp.version != App.shared.brewPhpVersion {
|
||||||
|
print("The `php` formula alias seems to be the different between the tap and core. This could be a problem!")
|
||||||
|
print("Determining whether both of these versions are installed...")
|
||||||
|
|
||||||
|
let bothInstalled = App.shared.availablePhpVersions.contains(tapPhp.version)
|
||||||
|
&& App.shared.availablePhpVersions.contains(App.shared.brewPhpVersion)
|
||||||
|
|
||||||
|
if bothInstalled {
|
||||||
|
print("Both conflicting aliases seem to be installed, warning the user!")
|
||||||
|
} else {
|
||||||
|
print("Conflicting aliases are not both installed, seems fine!")
|
||||||
|
}
|
||||||
|
|
||||||
|
return bothInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
print("All seems to be OK. No conflicts, both are PHP \(tapPhp.version).")
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
30
phpmon/Domain/Integrations/Homebrew/HomebrewPackage.swift
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
//
|
||||||
|
// HomebrewPackage.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct HomebrewPackage: Decodable {
|
||||||
|
|
||||||
|
let name: String
|
||||||
|
let full_name: String
|
||||||
|
let aliases: [String]
|
||||||
|
let installed: [HomebrewInstalled]
|
||||||
|
let linked_keg: String?
|
||||||
|
|
||||||
|
public var version: String {
|
||||||
|
return aliases.first!
|
||||||
|
.replacingOccurrences(of: "php@", with: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HomebrewInstalled: Decodable {
|
||||||
|
let version: String
|
||||||
|
let built_as_bottle: Bool
|
||||||
|
let installed_as_dependency: Bool
|
||||||
|
let installed_on_request: Bool
|
||||||
|
}
|
219
phpmon/Domain/Integrations/Valet/Valet.swift
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
//
|
||||||
|
// 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() {
|
||||||
|
let maximumPreload = 10
|
||||||
|
let foundSites = self.countPaths()
|
||||||
|
if foundSites <= maximumPreload {
|
||||||
|
// Preload the sites and their drivers
|
||||||
|
print("Fewer than or \(maximumPreload) sites found, preloading list of sites...")
|
||||||
|
self.reloadSites()
|
||||||
|
} else {
|
||||||
|
print("\(foundSites) sites found, exceeds \(maximumPreload) for preload at launch!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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))")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Returns a count of how many sites are linked and parked.
|
||||||
|
*/
|
||||||
|
private func countPaths() -> Int {
|
||||||
|
var count = 0
|
||||||
|
for path in config.paths {
|
||||||
|
let entries = try! FileManager.default.contentsOfDirectory(atPath: path)
|
||||||
|
for entry in entries {
|
||||||
|
if resolveSite(entry, forPath: path) {
|
||||||
|
count += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Resolves all paths and creates linked or parked site instances that can be referenced later.
|
||||||
|
*/
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Determines whether the site can be resolved as a symbolic link or as a directory.
|
||||||
|
Regular files are ignored. Returns true if the path can be parsed.
|
||||||
|
*/
|
||||||
|
private func resolveSite(_ entry: String, forPath path: String) -> Bool {
|
||||||
|
let siteDir = path + "/" + entry
|
||||||
|
|
||||||
|
let attrs = try! FileManager.default.attributesOfItem(atPath: siteDir)
|
||||||
|
|
||||||
|
let type = attrs[FileAttributeKey.type] as! FileAttributeType
|
||||||
|
|
||||||
|
if type == FileAttributeType.typeSymbolicLink || type == FileAttributeType.typeDirectory {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Determines whether the site can be resolved as a symbolic link or as a directory.
|
||||||
|
Regular files are ignored, and the site is added to Valet's list of sites.
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
|
||||||
|
// We should also check that we can interpret the path correctly
|
||||||
|
if URL(fileURLWithPath: siteDir).lastPathComponent == "" {
|
||||||
|
print("Warning: could not parse the site: \(siteDir), skipping!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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(fileURLWithPath: 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(fileURLWithPath: 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -10,6 +10,7 @@ import Foundation
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
class HeaderView: NSView, XibLoadable {
|
class HeaderView: NSView, XibLoadable {
|
||||||
|
|
||||||
@IBOutlet weak var textField: NSTextField!
|
@IBOutlet weak var textField: NSTextField!
|
||||||
|
|
||||||
static func asMenuItem(text: String) -> NSMenuItem {
|
static func asMenuItem(text: String) -> NSMenuItem {
|
||||||
@ -20,4 +21,5 @@ class HeaderView: NSView, XibLoadable {
|
|||||||
item.target = self
|
item.target = self
|
||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -7,10 +7,12 @@
|
|||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
class MainMenu: NSObject, NSWindowDelegate {
|
class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
|
||||||
|
|
||||||
static let shared = MainMenu()
|
static let shared = MainMenu()
|
||||||
|
|
||||||
|
weak var menuDelegate: NSMenuDelegate? = nil
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The status bar item with variable length.
|
The status bar item with variable length.
|
||||||
*/
|
*/
|
||||||
@ -25,11 +27,12 @@ class MainMenu: NSObject, NSWindowDelegate {
|
|||||||
*/
|
*/
|
||||||
func startup() {
|
func startup() {
|
||||||
// Start with the icon
|
// Start with the icon
|
||||||
self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
||||||
|
|
||||||
// Perform environment boot checks
|
// Perform environment boot checks
|
||||||
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
|
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
|
||||||
Startup().checkEnvironment(success: { self.onEnvironmentPass() },
|
Startup().checkEnvironment(success: { onEnvironmentPass() },
|
||||||
failure: { self.onEnvironmentFail() }
|
failure: { onEnvironmentFail() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -38,14 +41,48 @@ class MainMenu: NSObject, NSWindowDelegate {
|
|||||||
When the environment is all clear and the app can run, let's go.
|
When the environment is all clear and the app can run, let's go.
|
||||||
*/
|
*/
|
||||||
private func onEnvironmentPass() {
|
private func onEnvironmentPass() {
|
||||||
App.shared.availablePhpVersions = Actions.detectPhpVersions()
|
_ = Actions.detectPhpVersions()
|
||||||
self.updatePhpVersionInStatusBar()
|
|
||||||
|
if HomebrewDiagnostics.shared.errors.contains(.aliasConflict) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
Alert.notify(
|
||||||
|
message: "alert.php_alias_conflict.title".localized,
|
||||||
|
info: "alert.php_alias_conflict.info".localized,
|
||||||
|
style: .critical
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePhpVersionInStatusBar()
|
||||||
|
|
||||||
|
print("Determining broken PHP-FPM...")
|
||||||
|
// Attempt to find out if PHP-FPM is broken
|
||||||
|
let installation = App.phpInstall!
|
||||||
|
installation.notifyAboutBrokenPhpFpm()
|
||||||
|
|
||||||
|
print("Detecting applications...")
|
||||||
|
// Attempt to load list of applications
|
||||||
|
App.shared.detectedApplications = Application.detectPresetApplications()
|
||||||
|
let appNames = App.shared.detectedApplications.map { app in
|
||||||
|
return app.name
|
||||||
|
}
|
||||||
|
print("Detected applications: \(appNames)")
|
||||||
|
|
||||||
|
// Load the global hotkey
|
||||||
|
App.shared.loadGlobalHotkey()
|
||||||
|
|
||||||
|
// Attempt to find out more info about Valet
|
||||||
|
print("PHP Monitor has extracted the version number of Valet: \(Valet.shared.version)")
|
||||||
|
Valet.shared.validateVersion()
|
||||||
|
Valet.shared.startPreloadingSites()
|
||||||
|
print("PHP Monitor is ready to serve!")
|
||||||
|
|
||||||
// Schedule a request to fetch the PHP version every 60 seconds
|
// Schedule a request to fetch the PHP version every 60 seconds
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async { [self] in
|
||||||
App.shared.timer = Timer.scheduledTimer(
|
App.shared.timer = Timer.scheduledTimer(
|
||||||
timeInterval: 60,
|
timeInterval: 60,
|
||||||
target: self,
|
target: self,
|
||||||
selector: #selector(self.updatePhpVersionInStatusBar),
|
selector: #selector(updatePhpVersionInStatusBar),
|
||||||
userInfo: nil,
|
userInfo: nil,
|
||||||
repeats: true
|
repeats: true
|
||||||
)
|
)
|
||||||
@ -56,7 +93,7 @@ class MainMenu: NSObject, NSWindowDelegate {
|
|||||||
When the environment is not OK, present an alert to inform the user.
|
When the environment is not OK, present an alert to inform the user.
|
||||||
*/
|
*/
|
||||||
private func onEnvironmentFail() {
|
private func onEnvironmentFail() {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async { [self] in
|
||||||
let close = Alert.present(
|
let close = Alert.present(
|
||||||
messageText: "alert.cannot_start.title".localized,
|
messageText: "alert.cannot_start.title".localized,
|
||||||
informativeText: "alert.cannot_start.info".localized,
|
informativeText: "alert.cannot_start.info".localized,
|
||||||
@ -68,7 +105,7 @@ class MainMenu: NSObject, NSWindowDelegate {
|
|||||||
exit(1)
|
exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.startup()
|
startup()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,7 +114,7 @@ class MainMenu: NSObject, NSWindowDelegate {
|
|||||||
*/
|
*/
|
||||||
func update() {
|
func update() {
|
||||||
// Update the menu item on the main thread
|
// Update the menu item on the main thread
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async { [self] in
|
||||||
// Create a new menu
|
// Create a new menu
|
||||||
let menu = StatusMenu()
|
let menu = StatusMenu()
|
||||||
|
|
||||||
@ -89,20 +126,30 @@ class MainMenu: NSObject, NSWindowDelegate {
|
|||||||
menu.addPhpActionMenuItems()
|
menu.addPhpActionMenuItems()
|
||||||
menu.addItem(NSMenuItem.separator())
|
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
|
// Add information about services & actions
|
||||||
menu.addPhpConfigurationMenuItems()
|
menu.addPhpConfigurationMenuItems()
|
||||||
menu.addItem(NSMenuItem.separator())
|
menu.addItem(NSMenuItem.separator())
|
||||||
|
|
||||||
// Add about & quit menu items
|
// Add about & quit menu items
|
||||||
menu.addItem(NSMenuItem(title: "mi_about".localized, action: #selector(self.openAbout), keyEquivalent: ""))
|
menu.addItem(NSMenuItem(title: "mi_preferences".localized, action: #selector(openPrefs), keyEquivalent: ","))
|
||||||
menu.addItem(NSMenuItem(title: "mi_quit".localized, action: #selector(self.terminateApp), keyEquivalent: "q"))
|
menu.addItem(NSMenuItem(title: "mi_about".localized, action: #selector(openAbout), keyEquivalent: ""))
|
||||||
|
menu.addItem(NSMenuItem(title: "mi_quit".localized, action: #selector(terminateApp), keyEquivalent: "q"))
|
||||||
|
|
||||||
// Make sure every item can be interacted with
|
// Make sure every item can be interacted with
|
||||||
menu.items.forEach({ (item) in
|
menu.items.forEach({ (item) in
|
||||||
item.target = self
|
item.target = self
|
||||||
})
|
})
|
||||||
|
|
||||||
self.statusItem.menu = menu
|
statusItem.menu = menu
|
||||||
|
statusItem.menu?.delegate = self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,8 +157,8 @@ class MainMenu: NSObject, NSWindowDelegate {
|
|||||||
Sets the status bar image based on a version string.
|
Sets the status bar image based on a version string.
|
||||||
*/
|
*/
|
||||||
func setStatusBarImage(version: String) {
|
func setStatusBarImage(version: String) {
|
||||||
self.setStatusBar(
|
setStatusBar(
|
||||||
image: MenuBarImageGenerator.textToImage(text: version)
|
image: MenuBarImageGenerator.textToImageWithIcon(text: version)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,17 +183,18 @@ class MainMenu: NSObject, NSWindowDelegate {
|
|||||||
- Parameter execute: Callback of the work that needs to happen.
|
- Parameter execute: Callback of the work that needs to happen.
|
||||||
- Parameter completion: Callback that is fired when the work is done.
|
- Parameter completion: Callback that is fired when the work is done.
|
||||||
*/
|
*/
|
||||||
private func waitAndExecute(_ execute: @escaping () -> Void, _ completion: @escaping () -> Void = {})
|
private func waitAndExecute(_ execute: @escaping () -> Void, completion: @escaping () -> Void = {})
|
||||||
{
|
{
|
||||||
App.shared.busy = true
|
App.shared.busy = true
|
||||||
self.setBusyImage()
|
setBusyImage()
|
||||||
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
|
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
|
||||||
self.update()
|
update()
|
||||||
execute()
|
execute()
|
||||||
App.shared.busy = false
|
App.shared.busy = false
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.updatePhpVersionInStatusBar()
|
DispatchQueue.main.async { [self] in
|
||||||
self.update()
|
updatePhpVersionInStatusBar()
|
||||||
|
update()
|
||||||
completion()
|
completion()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -155,81 +203,128 @@ class MainMenu: NSObject, NSWindowDelegate {
|
|||||||
// MARK: - User Interface
|
// MARK: - User Interface
|
||||||
|
|
||||||
@objc func updatePhpVersionInStatusBar() {
|
@objc func updatePhpVersionInStatusBar() {
|
||||||
App.shared.currentInstall = PhpInstallation()
|
App.shared.currentInstall = ActivePhpInstallation()
|
||||||
|
refreshIcon()
|
||||||
DispatchQueue.main.async {
|
update()
|
||||||
if (App.shared.busy) {
|
}
|
||||||
self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
|
||||||
|
func refreshIcon() {
|
||||||
|
DispatchQueue.main.async { [self] in
|
||||||
|
if (App.busy) {
|
||||||
|
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
||||||
} else {
|
} else {
|
||||||
self.setStatusBarImage(version: App.phpInstall!.version.short)
|
if Preferences.preferences[.shouldDisplayDynamicIcon] as! Bool == false {
|
||||||
|
// Static icon has been requested
|
||||||
|
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIconStatic"))!)
|
||||||
|
} else {
|
||||||
|
// The dynamic icon has been requested
|
||||||
|
let long = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool
|
||||||
|
setStatusBarImage(version: long ? App.phpInstall!.version.long : App.phpInstall!.version.short)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func reloadPhpMonitorMenu() {
|
||||||
|
waitAndExecute {
|
||||||
|
// This automatically reloads the menu
|
||||||
|
print("Reloading information about the PHP installation...")
|
||||||
|
} completion: {
|
||||||
|
// Add a slight delay to make sure it loads the new menu
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||||
|
// Open the menu again
|
||||||
|
MainMenu.shared.statusItem.button?.performClick(nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.update()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func setBusyImage() {
|
@objc func setBusyImage() {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async { [self] in
|
||||||
self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Actions
|
// MARK: - Actions
|
||||||
|
|
||||||
@objc func restartPhpFpm() {
|
@objc func restartPhpFpm() {
|
||||||
self.waitAndExecute({
|
waitAndExecute {
|
||||||
Actions.restartPhpFpm()
|
Actions.restartPhpFpm()
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func restartAllServices() {
|
@objc func restartAllServices() {
|
||||||
self.waitAndExecute({
|
waitAndExecute {
|
||||||
Actions.restartDnsMasq()
|
Actions.restartDnsMasq()
|
||||||
Actions.restartPhpFpm()
|
Actions.restartPhpFpm()
|
||||||
Actions.restartNginx()
|
Actions.restartNginx()
|
||||||
})
|
} completion: {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
LocalNotification.send(
|
||||||
|
title: "notification.services_restarted".localized,
|
||||||
|
subtitle: "notification.services_restarted_desc".localized
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func stopAllServices() {
|
||||||
|
waitAndExecute {
|
||||||
|
Actions.stopAllServices()
|
||||||
|
} completion: {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
LocalNotification.send(
|
||||||
|
title: "notification.services_stopped".localized,
|
||||||
|
subtitle: "notification.services_stopped_desc".localized
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func restartNginx() {
|
@objc func restartNginx() {
|
||||||
self.waitAndExecute({
|
waitAndExecute {
|
||||||
Actions.restartNginx()
|
Actions.restartNginx()
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func restartDnsMasq() {
|
@objc func restartDnsMasq() {
|
||||||
self.waitAndExecute({
|
waitAndExecute {
|
||||||
Actions.restartDnsMasq()
|
Actions.restartDnsMasq()
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func toggleExtension(sender: ExtensionMenuItem) {
|
@objc func toggleExtension(sender: ExtensionMenuItem) {
|
||||||
self.waitAndExecute({
|
waitAndExecute {
|
||||||
// Toggle that extension
|
|
||||||
print("Toggling extension '\(sender.phpExtension!.name)'")
|
|
||||||
sender.phpExtension?.toggle()
|
sender.phpExtension?.toggle()
|
||||||
})
|
|
||||||
|
if Preferences.preferences[.autoServiceRestartAfterExtensionToggle] as! Bool == true {
|
||||||
|
Actions.restartPhpFpm()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func openPhpInfo() {
|
@objc func openPhpInfo() {
|
||||||
self.waitAndExecute({
|
waitAndExecute {
|
||||||
|
// Write a file called `phpmon_phpinfo.php` to /tmp
|
||||||
try! "<?php phpinfo();".write(toFile: "/tmp/phpmon_phpinfo.php", atomically: true, encoding: .utf8)
|
try! "<?php phpinfo();".write(toFile: "/tmp/phpmon_phpinfo.php", atomically: true, encoding: .utf8)
|
||||||
|
|
||||||
|
// Tell php-cgi to run the PHP and output as an .html file
|
||||||
Shell.run("\(Paths.binPath)/php-cgi -q /tmp/phpmon_phpinfo.php > /tmp/phpmon_phpinfo.html")
|
Shell.run("\(Paths.binPath)/php-cgi -q /tmp/phpmon_phpinfo.php > /tmp/phpmon_phpinfo.html")
|
||||||
}, {
|
} completion: {
|
||||||
|
// When this has been completed, open the URL to the file in the browser
|
||||||
NSWorkspace.shared.open(URL(string: "file:///private/tmp/phpmon_phpinfo.html")!)
|
NSWorkspace.shared.open(URL(string: "file:///private/tmp/phpmon_phpinfo.html")!)
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func forceRestartLatestPhp() {
|
@objc func forceRestartLatestPhp() {
|
||||||
// Tell the user the switch is about to occur
|
// Tell the user the switch is about to occur
|
||||||
Alert.notify(message: "alert.force_reload.title".localized, info: "alert.force_reload.info".localized)
|
Alert.notify(message: "alert.force_reload.title".localized, info: "alert.force_reload.info".localized)
|
||||||
|
|
||||||
// Start switching
|
// Start switching
|
||||||
self.waitAndExecute(
|
waitAndExecute {
|
||||||
{ Actions.fixMyPhp() },
|
Actions.fixMyPhp()
|
||||||
{ Alert.notify(
|
} completion: {
|
||||||
message: "alert.force_reload_done.title".localized,
|
Alert.notify(message: "alert.force_reload_done.title".localized, info: "alert.force_reload_done.info".localized)
|
||||||
info: "alert.force_reload_done.info".localized
|
}
|
||||||
) }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func openActiveConfigFolder() {
|
@objc func openActiveConfigFolder() {
|
||||||
@ -243,42 +338,62 @@ class MainMenu: NSObject, NSWindowDelegate {
|
|||||||
Actions.openPhpConfigFolder(version: App.phpInstall!.version.short)
|
Actions.openPhpConfigFolder(version: App.phpInstall!.version.short)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func openGlobalComposerFolder() {
|
||||||
|
Actions.openGlobalComposerFolder()
|
||||||
|
}
|
||||||
|
|
||||||
@objc func openValetConfigFolder() {
|
@objc func openValetConfigFolder() {
|
||||||
Actions.openValetConfigFolder()
|
Actions.openValetConfigFolder()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func switchToPhpVersion(sender: PhpMenuItem) {
|
@objc func switchToPhpVersion(sender: PhpMenuItem) {
|
||||||
print("Switching to: PHP \(sender.version)")
|
setBusyImage()
|
||||||
|
|
||||||
self.setBusyImage()
|
|
||||||
App.shared.busy = true
|
App.shared.busy = true
|
||||||
|
|
||||||
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
|
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
|
||||||
// Update the PHP version in the status bar
|
// Update the PHP version in the status bar
|
||||||
self.updatePhpVersionInStatusBar()
|
updatePhpVersionInStatusBar()
|
||||||
|
|
||||||
// Update the menu
|
// Update the menu
|
||||||
self.update()
|
update()
|
||||||
|
|
||||||
// Switch the PHP version
|
let completion = {
|
||||||
Actions.switchToPhpVersion(
|
// Mark as no longer busy
|
||||||
version: sender.version,
|
App.shared.busy = false
|
||||||
availableVersions: App.shared.availablePhpVersions
|
|
||||||
)
|
// Perform UI updates on main thread
|
||||||
|
DispatchQueue.main.async { [self] in
|
||||||
// Mark as no longer busy
|
updatePhpVersionInStatusBar()
|
||||||
App.shared.busy = false
|
update()
|
||||||
|
|
||||||
// Perform UI updates on main thread
|
// Send a notification that the switch has been completed
|
||||||
DispatchQueue.main.async {
|
LocalNotification.send(
|
||||||
self.updatePhpVersionInStatusBar()
|
title: String(format: "notification.version_changed_title".localized, sender.version),
|
||||||
self.update()
|
subtitle: String(format: "notification.version_changed_desc".localized, sender.version)
|
||||||
// Send a notification that the switch has been completed
|
)
|
||||||
LocalNotification.send(
|
|
||||||
title: String(format: "notification.version_changed_title".localized, sender.version),
|
App.phpInstall?.notifyAboutBrokenPhpFpm()
|
||||||
subtitle: String(format: "notification.version_changed_desc".localized, sender.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
|
||||||
|
)
|
||||||
|
/* } */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -287,7 +402,27 @@ class MainMenu: NSObject, NSWindowDelegate {
|
|||||||
NSApplication.shared.orderFrontStandardAboutPanel()
|
NSApplication.shared.orderFrontStandardAboutPanel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func openPrefs() {
|
||||||
|
PrefsVC.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func openSiteList() {
|
||||||
|
SiteListVC.show()
|
||||||
|
}
|
||||||
|
|
||||||
@objc func terminateApp() {
|
@objc func terminateApp() {
|
||||||
NSApplication.shared.terminate(nil)
|
NSApplication.shared.terminate(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Menu Delegate
|
||||||
|
|
||||||
|
func menuWillOpen(_ menu: NSMenu) {
|
||||||
|
// Make sure the shortcut key does not trigger this when the menu is open
|
||||||
|
App.shared.shortcutHotkey?.isPaused = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func menuDidClose(_ menu: NSMenu) {
|
||||||
|
// When the menu is closed, allow the shortcut to work again
|
||||||
|
App.shared.shortcutHotkey?.isPaused = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import Foundation
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
class StatsView: NSView, XibLoadable {
|
class StatsView: NSView, XibLoadable {
|
||||||
|
|
||||||
@IBOutlet weak var titleMemLimit: NSTextField!
|
@IBOutlet weak var titleMemLimit: NSTextField!
|
||||||
@IBOutlet weak var titleMaxPost: NSTextField!
|
@IBOutlet weak var titleMaxPost: NSTextField!
|
||||||
@IBOutlet weak var titleMaxUpload: NSTextField!
|
@IBOutlet weak var titleMaxUpload: NSTextField!
|
||||||
@ -31,4 +32,5 @@ class StatsView: NSView, XibLoadable {
|
|||||||
item.target = self
|
item.target = self
|
||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -15,18 +15,18 @@ class StatusMenu : NSMenu {
|
|||||||
|
|
||||||
if App.phpInstall!.version.error {
|
if App.phpInstall!.version.error {
|
||||||
for message in ["mi_php_broken_1", "mi_php_broken_2", "mi_php_broken_3", "mi_php_broken_4"] {
|
for message in ["mi_php_broken_1", "mi_php_broken_2", "mi_php_broken_3", "mi_php_broken_4"] {
|
||||||
self.addItem(NSMenuItem(title: message.localized, action: nil, keyEquivalent: ""))
|
addItem(NSMenuItem(title: message.localized, action: nil, keyEquivalent: ""))
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let phpVersionText = "\("mi_php_version".localized) \(App.phpInstall!.version.long)"
|
let phpVersionText = "\("mi_php_version".localized) \(App.phpInstall!.version.long)"
|
||||||
self.addItem(HeaderView.asMenuItem(text: phpVersionText))
|
addItem(HeaderView.asMenuItem(text: phpVersionText))
|
||||||
}
|
}
|
||||||
|
|
||||||
func addPhpActionMenuItems() {
|
func addPhpActionMenuItems() {
|
||||||
if App.busy {
|
if App.busy {
|
||||||
self.addItem(NSMenuItem(title: "mi_busy".localized, action: nil, keyEquivalent: ""))
|
addItem(NSMenuItem(title: "mi_busy".localized, action: nil, keyEquivalent: ""))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,41 +36,48 @@ class StatusMenu : NSMenu {
|
|||||||
|
|
||||||
self.addSwitchToPhpMenuItems()
|
self.addSwitchToPhpMenuItems()
|
||||||
self.addItem(NSMenuItem.separator())
|
self.addItem(NSMenuItem.separator())
|
||||||
self.addServicesMenuItems()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addSwitchToPhpMenuItems() {
|
func addServicesMenuItems() {
|
||||||
var shortcutKey = 1
|
|
||||||
for index in (0..<App.shared.availablePhpVersions.count).reversed() {
|
|
||||||
let version = App.shared.availablePhpVersions[index]
|
|
||||||
let action = #selector(MainMenu.switchToPhpVersion(sender:))
|
|
||||||
let brew = (version == App.shared.brewPhpVersion) ? "php" : "php@\(version)"
|
|
||||||
let menuItem = PhpMenuItem(
|
|
||||||
title: "\("mi_php_switch".localized) \(version) (\(brew))",
|
|
||||||
action: (version == App.phpInstall?.version.short) ? nil : action, keyEquivalent: "\(shortcutKey)"
|
|
||||||
)
|
|
||||||
menuItem.version = version
|
|
||||||
shortcutKey = shortcutKey + 1
|
|
||||||
self.addItem(menuItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func addServicesMenuItems() {
|
|
||||||
self.addItem(HeaderView.asMenuItem(text: "mi_active_services".localized))
|
self.addItem(HeaderView.asMenuItem(text: "mi_active_services".localized))
|
||||||
|
|
||||||
let services = NSMenuItem(title: "mi_restart_specific".localized, action: nil, keyEquivalent: "")
|
let services = NSMenuItem(title: "mi_manage_services".localized, action: nil, keyEquivalent: "")
|
||||||
let servicesMenu = NSMenu()
|
let servicesMenu = NSMenu()
|
||||||
servicesMenu.addItem(NSMenuItem(title: "mi_restart_dnsmasq".localized, action: #selector(MainMenu.restartDnsMasq), keyEquivalent: "d"))
|
servicesMenu.addItem(NSMenuItem(title: "mi_restart_dnsmasq".localized, action: #selector(MainMenu.restartDnsMasq), keyEquivalent: "d"))
|
||||||
servicesMenu.addItem(NSMenuItem(title: "mi_restart_php_fpm".localized, action: #selector(MainMenu.restartPhpFpm), keyEquivalent: "p"))
|
servicesMenu.addItem(NSMenuItem(title: "mi_restart_php_fpm".localized, action: #selector(MainMenu.restartPhpFpm), keyEquivalent: "p"))
|
||||||
servicesMenu.addItem(NSMenuItem(title: "mi_restart_nginx".localized, action: #selector(MainMenu.restartNginx), keyEquivalent: "n"))
|
servicesMenu.addItem(NSMenuItem(title: "mi_restart_nginx".localized, action: #selector(MainMenu.restartNginx), keyEquivalent: "n"))
|
||||||
|
servicesMenu.addItem(
|
||||||
|
NSMenuItem(title: "mi_stop_all_services".localized, action: #selector(MainMenu.stopAllServices), keyEquivalent: "s"),
|
||||||
|
withKeyModifier: [.command, .shift])
|
||||||
|
servicesMenu.addItem(NSMenuItem(title: "mi_restart_all_services".localized, action: #selector(MainMenu.restartAllServices), keyEquivalent: "s"))
|
||||||
for item in servicesMenu.items {
|
for item in servicesMenu.items {
|
||||||
item.target = MainMenu.shared
|
item.target = MainMenu.shared
|
||||||
}
|
}
|
||||||
self.setSubmenu(servicesMenu, for: services)
|
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(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() {
|
func addPhpConfigurationMenuItems() {
|
||||||
@ -80,7 +87,7 @@ class StatusMenu : NSMenu {
|
|||||||
|
|
||||||
// Configuration
|
// Configuration
|
||||||
self.addItem(HeaderView.asMenuItem(text: "mi_configuration".localized))
|
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_php_config".localized, action: #selector(MainMenu.openActiveConfigFolder), keyEquivalent: "c"))
|
||||||
self.addItem(NSMenuItem(title: "mi_phpinfo".localized, action: #selector(MainMenu.openPhpInfo), keyEquivalent: "i"))
|
self.addItem(NSMenuItem(title: "mi_phpinfo".localized, action: #selector(MainMenu.openPhpInfo), keyEquivalent: "i"))
|
||||||
|
|
||||||
@ -88,14 +95,14 @@ class StatusMenu : NSMenu {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let stats = App.phpInstall!.configuration
|
let stats = App.phpInstall!.limits
|
||||||
|
|
||||||
// Stats
|
// Stats
|
||||||
self.addItem(NSMenuItem.separator())
|
self.addItem(NSMenuItem.separator())
|
||||||
self.addItem(StatsView.asMenuItem(
|
self.addItem(StatsView.asMenuItem(
|
||||||
memory: stats.memory_limit,
|
memory: stats!.memory_limit,
|
||||||
post: stats.post_max_size,
|
post: stats!.post_max_size,
|
||||||
upload: stats.upload_max_filesize)
|
upload: stats!.upload_max_filesize)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Extensions
|
// Extensions
|
||||||
@ -106,18 +113,58 @@ class StatusMenu : NSMenu {
|
|||||||
self.addItem(NSMenuItem(title: "mi_no_extensions_detected".localized, action: nil, keyEquivalent: ""))
|
self.addItem(NSMenuItem(title: "mi_no_extensions_detected".localized, action: nil, keyEquivalent: ""))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var shortcutKey = 1
|
||||||
for phpExtension in App.phpInstall!.extensions {
|
for phpExtension in App.phpInstall!.extensions {
|
||||||
self.addExtensionItem(phpExtension)
|
self.addExtensionItem(phpExtension, shortcutKey)
|
||||||
|
shortcutKey += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
self.addItem(NSMenuItem.separator())
|
||||||
|
|
||||||
|
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) {
|
private func addExtensionItem(_ phpExtension: PhpExtension, _ shortcutKey: Int) {
|
||||||
|
let keyEquivalent = shortcutKey < 9 ? "\(shortcutKey)" : ""
|
||||||
|
|
||||||
let menuItem = ExtensionMenuItem(
|
let menuItem = ExtensionMenuItem(
|
||||||
title: "\(phpExtension.name.capitalized) (php.ini)",
|
title: "\(phpExtension.name) (\(phpExtension.fileNameOnly))",
|
||||||
action: #selector(MainMenu.toggleExtension), keyEquivalent: ""
|
action: #selector(MainMenu.toggleExtension),
|
||||||
|
keyEquivalent: keyEquivalent
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if menuItem.keyEquivalent != "" {
|
||||||
|
menuItem.keyEquivalentModifierMask = [.option]
|
||||||
|
}
|
||||||
|
|
||||||
menuItem.state = phpExtension.enabled ? .on : .off
|
menuItem.state = phpExtension.enabled ? .on : .off
|
||||||
menuItem.phpExtension = phpExtension
|
menuItem.phpExtension = phpExtension
|
||||||
|
|
||||||
self.addItem(menuItem)
|
self.addItem(menuItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -131,3 +178,7 @@ class PhpMenuItem: NSMenuItem {
|
|||||||
class ExtensionMenuItem: NSMenuItem {
|
class ExtensionMenuItem: NSMenuItem {
|
||||||
var phpExtension: PhpExtension? = nil
|
var phpExtension: PhpExtension? = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class EditorMenuItem: NSMenuItem {
|
||||||
|
var editor: Application? = nil
|
||||||
|
}
|
||||||
|
187
phpmon/Domain/PHP/ActivePhpInstallation.swift
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
//
|
||||||
|
// ActivePhpInstallation.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/**
|
||||||
|
An installed version of PHP, that was detected by scanning the `/opt/php@version/bin` directory.
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
class ActivePhpInstallation {
|
||||||
|
|
||||||
|
var version: Version!
|
||||||
|
var limits: Limits!
|
||||||
|
var extensions: [PhpExtension]!
|
||||||
|
|
||||||
|
// MARK: - Computed
|
||||||
|
|
||||||
|
var formula: String {
|
||||||
|
return (version.short == App.shared.brewPhpVersion) ? "php" : "php@\(version.short)"
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Initializer
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// Show information about the current version
|
||||||
|
getVersion()
|
||||||
|
|
||||||
|
// If an error occurred, exit early
|
||||||
|
if (version.error) {
|
||||||
|
limits = Limits()
|
||||||
|
extensions = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load extension information
|
||||||
|
let path = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini")
|
||||||
|
extensions = PhpExtension.load(from: path)
|
||||||
|
|
||||||
|
// Get configuration values
|
||||||
|
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
|
||||||
|
let paths = Command.execute(path: Paths.php, arguments: ["-r", "echo php_ini_scanned_files();"])
|
||||||
|
.replacingOccurrences(of: "\n", with: "")
|
||||||
|
.split(separator: ",")
|
||||||
|
.map { String($0) }
|
||||||
|
|
||||||
|
// See if any extensions are present in said .ini files
|
||||||
|
paths.forEach { (iniFilePath) in
|
||||||
|
let exts = PhpExtension.load(from: URL(fileURLWithPath: iniFilePath))
|
||||||
|
if exts.count > 0 {
|
||||||
|
extensions.append(contentsOf: exts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
When the app tries to retrieve the version, the installation is considered broken if the output is nothing,
|
||||||
|
_or_ if the output contains the word "Warning" or "Error". In normal situations this should not be the case.
|
||||||
|
*/
|
||||||
|
private func getVersion() -> Void {
|
||||||
|
self.version = Version()
|
||||||
|
|
||||||
|
let version = Command.execute(path: Paths.phpConfig, arguments: ["--version"], trimNewlines: true)
|
||||||
|
|
||||||
|
if (version == "" || version.contains("Warning") || version.contains("Error")) {
|
||||||
|
self.version.short = "💩 BROKEN"
|
||||||
|
self.version.long = ""
|
||||||
|
self.version.error = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// That's the long version
|
||||||
|
self.version.long = version
|
||||||
|
|
||||||
|
// Next up, let's strip away the minor version number
|
||||||
|
let segments = self.version.long.components(separatedBy: ".")
|
||||||
|
|
||||||
|
// Get the first two elements
|
||||||
|
self.version.short = segments[0...1].joined(separator: ".")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Retrieves the display value for a specific key in the `.ini` file.
|
||||||
|
|
||||||
|
The following values are valid:
|
||||||
|
* -1: unlimited (show the infinity icon)
|
||||||
|
* 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 ⚠️.
|
||||||
|
|
||||||
|
- Parameter key: The key of the `ini` value that needs to be retrieved. For example, you can use `memory_limit`.
|
||||||
|
*/
|
||||||
|
private func getByteCount(key: String) -> String {
|
||||||
|
let value = Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('\(key)');"])
|
||||||
|
|
||||||
|
// Check if the value is unlimited
|
||||||
|
if (value == "-1") {
|
||||||
|
return "∞"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the syntax is valid otherwise
|
||||||
|
let regex = try! NSRegularExpression(pattern: #"^([0-9]*)(K|M|G|)$"#, options: [])
|
||||||
|
let match = regex.matches(in: value, options: [], range: NSMakeRange(0, value.count)).first
|
||||||
|
return (match == nil) ? "⚠️" : "\(value)B"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
It is always possible that the system configuration for PHP-FPM has not been set up for Valet.
|
||||||
|
This can occur when a user manually installs a new PHP version, but does not run `valet install`.
|
||||||
|
In that case, we should alert the user!
|
||||||
|
|
||||||
|
- Important: The underlying check is `checkPhpFpmStatus`, which can be run multiple times.
|
||||||
|
This method actively presents a modal if said checks fails, so don't call this method too many times.
|
||||||
|
*/
|
||||||
|
public func notifyAboutBrokenPhpFpm() {
|
||||||
|
if !self.checkPhpFpmStatus() {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
Alert.notify(
|
||||||
|
message: "alert.php_fpm_broken.title".localized,
|
||||||
|
info: "alert.php_fpm_broken.info".localized,
|
||||||
|
style: .critical
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Determine if PHP-FPM is configured correctly.
|
||||||
|
|
||||||
|
For PHP 5.6, we'll check if `valet.sock` is included in the main `php-fpm.conf` file, but for more recent
|
||||||
|
versions of PHP, we can just check for the existence of the `valet-fpm.conf` file. If the check here fails,
|
||||||
|
that means that Valet won't work properly.
|
||||||
|
*/
|
||||||
|
private func checkPhpFpmStatus() -> Bool {
|
||||||
|
if self.version.short == "5.6" {
|
||||||
|
// The main PHP config file should contain `valet.sock` and then we're probably fine?
|
||||||
|
let fileName = "\(Paths.etcPath)/php/5.6/php-fpm.conf"
|
||||||
|
return Shell.pipe("cat \(fileName)").contains("valet.sock")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure to check if valet-fpm.conf exists. If it does, we should be fine :)
|
||||||
|
return Shell.fileExists("\(Paths.etcPath)/php/\(self.version.short)/php-fpm.d/valet-fpm.conf")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 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 = "???"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -29,6 +29,11 @@ class PhpExtension {
|
|||||||
/// Whether the extension has been enabled.
|
/// Whether the extension has been enabled.
|
||||||
var enabled: Bool
|
var enabled: Bool
|
||||||
|
|
||||||
|
/// The file where this extension was located, but only the filename, not the full path to the .ini file.
|
||||||
|
var fileNameOnly: String {
|
||||||
|
return String(file.split(separator: "/").last ?? "php.ini")
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
This regular expression will allow us to identify lines which activate an extension.
|
This regular expression will allow us to identify lines which activate an extension.
|
||||||
|
|
||||||
@ -41,7 +46,7 @@ class PhpExtension {
|
|||||||
|
|
||||||
- Note: Extensions that are disabled in a different way will not be detected. This is intentional.
|
- Note: Extensions that are disabled in a different way will not be detected. This is intentional.
|
||||||
*/
|
*/
|
||||||
static let extensionRegex = #"^(extension=|zend_extension=|; extension=|; zend_extension=)"(?<name>[a-zA-Z]*).so"$"#
|
static let extensionRegex = #"^(extension|zend_extension|;(\s?)extension|;(\s?)zend_extension)(\s?)(=)(\s?)(?<name>["]?(?:\/?.\/?)+(?:\.so)"?)$"#
|
||||||
|
|
||||||
/**
|
/**
|
||||||
When registering an extension, we do that based on the line found inside the .ini file.
|
When registering an extension, we do that based on the line found inside the .ini file.
|
||||||
@ -52,7 +57,13 @@ class PhpExtension {
|
|||||||
let range = Range(match!.range(withName: "name"), in: line)!
|
let range = Range(match!.range(withName: "name"), in: line)!
|
||||||
|
|
||||||
self.line = line
|
self.line = line
|
||||||
self.name = line[range]
|
|
||||||
|
let fullPath = String(line[range])
|
||||||
|
.replacingOccurrences(of: "\"", with: "") // replace excess "
|
||||||
|
.replacingOccurrences(of: ".so", with: "") // replace excess .so
|
||||||
|
|
||||||
|
self.name = String(fullPath.split(separator: "/").last!) // take last segment
|
||||||
|
|
||||||
self.enabled = !line.contains(";")
|
self.enabled = !line.contains(";")
|
||||||
self.file = file
|
self.file = file
|
||||||
}
|
}
|
||||||
@ -61,12 +72,15 @@ class PhpExtension {
|
|||||||
This simply toggles the extension in the .ini file. You may need to restart the other services in order for this change to apply.
|
This simply toggles the extension in the .ini file. You may need to restart the other services in order for this change to apply.
|
||||||
*/
|
*/
|
||||||
func toggle() {
|
func toggle() {
|
||||||
Actions.sed(
|
let newLine = enabled
|
||||||
file: self.file,
|
// DISABLED: Commented out line
|
||||||
original: self.line,
|
? "; \(line)"
|
||||||
replacement: self.enabled ? "; \(self.line)" : self.line.replacingOccurrences(of: "; ", with: "")
|
// ENABLED: Line where the comment delimiter (;) is removed
|
||||||
)
|
: line.replacingOccurrences(of: "; ", with: "")
|
||||||
self.enabled = !self.enabled
|
|
||||||
|
Actions.sed(file: file, original: line, replacement: newLine)
|
||||||
|
|
||||||
|
enabled.toggle()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Static Methods
|
// MARK: - Static Methods
|
||||||
@ -83,11 +97,12 @@ class PhpExtension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return file!.components(separatedBy: "\n")
|
return file!.components(separatedBy: "\n")
|
||||||
.filter({ (line) -> Bool in
|
.filter {
|
||||||
return line.range(of: Self.extensionRegex, options: .regularExpression) != nil
|
return $0.range(of: Self.extensionRegex, options: .regularExpression) != nil
|
||||||
})
|
}
|
||||||
.map { (line) -> PhpExtension in
|
.map {
|
||||||
return PhpExtension(line, file: path.path)
|
return PhpExtension($0, file: path.path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
30
phpmon/Domain/PHP/PhpInstallation.swift
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
//
|
||||||
|
// PhpInstallation.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 28/11/2021.
|
||||||
|
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class PhpInstallation {
|
||||||
|
|
||||||
|
var longVersion: String
|
||||||
|
|
||||||
|
/**
|
||||||
|
In order to determine details about a PHP installation, we’ll simply run `php-config --version`
|
||||||
|
in the relevant directory.
|
||||||
|
*/
|
||||||
|
init(_ version: String) {
|
||||||
|
let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config"
|
||||||
|
self.longVersion = version
|
||||||
|
if Shell.fileExists(phpConfigExecutablePath) {
|
||||||
|
self.longVersion = Command.execute(
|
||||||
|
path: phpConfigExecutablePath,
|
||||||
|
arguments: ["--version"]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,75 @@
|
|||||||
|
//
|
||||||
|
// GlobalKeybindPreference.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 15/04/2021.
|
||||||
|
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct GlobalKeybindPreference: Codable, CustomStringConvertible {
|
||||||
|
|
||||||
|
// MARK: - Internal variables
|
||||||
|
|
||||||
|
let function : Bool
|
||||||
|
let control : Bool
|
||||||
|
let command : Bool
|
||||||
|
let shift : Bool
|
||||||
|
let option : Bool
|
||||||
|
let capsLock : Bool
|
||||||
|
let carbonFlags : UInt32
|
||||||
|
let characters : String?
|
||||||
|
let keyCode : UInt32
|
||||||
|
|
||||||
|
// MARK: - How the keybind is display in Preferences
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
var stringBuilder = ""
|
||||||
|
if self.function {
|
||||||
|
stringBuilder += "Fn"
|
||||||
|
}
|
||||||
|
if self.control {
|
||||||
|
stringBuilder += "⌃"
|
||||||
|
}
|
||||||
|
if self.option {
|
||||||
|
stringBuilder += "⌥"
|
||||||
|
}
|
||||||
|
if self.command {
|
||||||
|
stringBuilder += "⌘"
|
||||||
|
}
|
||||||
|
if self.shift {
|
||||||
|
stringBuilder += "⇧"
|
||||||
|
}
|
||||||
|
if self.capsLock {
|
||||||
|
stringBuilder += "⇪"
|
||||||
|
}
|
||||||
|
if let characters = self.characters {
|
||||||
|
stringBuilder += characters.uppercased()
|
||||||
|
}
|
||||||
|
return "\(stringBuilder)"
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Persisting data to UserDefaults (as JSON)
|
||||||
|
|
||||||
|
public func toJson() -> String {
|
||||||
|
let jsonData = try! JSONEncoder().encode(self)
|
||||||
|
return String(data: jsonData, encoding: .utf8)!
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func fromJson(_ string: String?) -> GlobalKeybindPreference? {
|
||||||
|
if string == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if let jsonData = string!.data(using: .utf8) {
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
do {
|
||||||
|
return try decoder.decode(GlobalKeybindPreference.self, from: jsonData)
|
||||||
|
} catch {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
94
phpmon/Domain/Preferences/Preferences.swift
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
//
|
||||||
|
// Preferences.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 30/03/2021.
|
||||||
|
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum PreferenceName: String {
|
||||||
|
case wasLaunchedBefore = "launched_before"
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
class Preferences {
|
||||||
|
|
||||||
|
// MARK: - Singleton
|
||||||
|
|
||||||
|
static var shared = Preferences()
|
||||||
|
|
||||||
|
var cachedPreferences: [PreferenceName: Any?]
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
Preferences.handleFirstTimeLaunch()
|
||||||
|
cachedPreferences = Self.cache()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - First Time Run
|
||||||
|
|
||||||
|
/**
|
||||||
|
Note: macOS seems to cache plist values in memory as well as in files.
|
||||||
|
You can find the persisted configuration file in: ~/Library/Preferences/com.nicoverbruggen.phpmon.plist
|
||||||
|
|
||||||
|
To clear the cache, and get a first-run experience you may need to run:
|
||||||
|
```
|
||||||
|
defaults delete com.nicoverbruggen.phpmon
|
||||||
|
killall cfprefsd
|
||||||
|
```
|
||||||
|
*/
|
||||||
|
static func handleFirstTimeLaunch() {
|
||||||
|
UserDefaults.standard.register(defaults: [
|
||||||
|
PreferenceName.shouldDisplayDynamicIcon.rawValue: true,
|
||||||
|
PreferenceName.fullPhpVersionDynamicIcon.rawValue: false,
|
||||||
|
PreferenceName.autoServiceRestartAfterExtensionToggle.rawValue: true,
|
||||||
|
PreferenceName.useInternalSwitcher.rawValue: false
|
||||||
|
])
|
||||||
|
|
||||||
|
if UserDefaults.standard.bool(forKey: PreferenceName.wasLaunchedBefore.rawValue) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
print("Saving first-time preferences!")
|
||||||
|
UserDefaults.standard.setValue(true, forKey: PreferenceName.wasLaunchedBefore.rawValue)
|
||||||
|
UserDefaults.standard.synchronize()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - API
|
||||||
|
|
||||||
|
static var preferences: [PreferenceName: Any?] {
|
||||||
|
return Self.shared.cachedPreferences
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Internal Functionality
|
||||||
|
|
||||||
|
static func cache() -> [PreferenceName: Any] {
|
||||||
|
return [
|
||||||
|
// Part 1: Always Booleans
|
||||||
|
.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,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
static func update(_ preference: PreferenceName, value: Any?) {
|
||||||
|
if (value == nil) {
|
||||||
|
UserDefaults.standard.removeObject(forKey: preference.rawValue)
|
||||||
|
} else {
|
||||||
|
UserDefaults.standard.setValue(value, forKey: preference.rawValue)
|
||||||
|
}
|
||||||
|
UserDefaults.standard.synchronize()
|
||||||
|
|
||||||
|
// Update the preferences cache in memory!
|
||||||
|
Preferences.shared.cachedPreferences = Preferences.cache()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
108
phpmon/Domain/Preferences/PrefsVC.swift
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
//
|
||||||
|
// PrefsVC.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 PrefsVC: NSViewController {
|
||||||
|
|
||||||
|
// MARK: - Window Identifier
|
||||||
|
|
||||||
|
@IBOutlet weak var stackView: NSStackView!
|
||||||
|
|
||||||
|
// MARK: - Display
|
||||||
|
|
||||||
|
public static func create(delegate: NSWindowDelegate?) {
|
||||||
|
let storyboard = NSStoryboard(name: "Main" , bundle : nil)
|
||||||
|
|
||||||
|
let windowController = storyboard.instantiateController(
|
||||||
|
withIdentifier: "preferencesWindow"
|
||||||
|
) as! PrefsWC
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
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
|
||||||
|
)
|
||||||
|
].forEach({ self.stackView.addArrangedSubview($0) })
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Listening for hotkey dleegate
|
||||||
|
|
||||||
|
var listeningForHotkeyView: HotkeyPreferenceView? = nil
|
||||||
|
|
||||||
|
override func viewWillDisappear() {
|
||||||
|
if listeningForHotkeyView !== nil {
|
||||||
|
listeningForHotkeyView = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Deinitialization
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
print("VC deallocated")
|
||||||
|
}
|
||||||
|
}
|
47
phpmon/Domain/Preferences/PrefsWC.swift
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
//
|
||||||
|
// PrefsWC.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 02/04/2021.
|
||||||
|
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Cocoa
|
||||||
|
|
||||||
|
struct Keys {
|
||||||
|
static let Escape = 53
|
||||||
|
static let Space = 49
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = 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.listeningForHotkeyView = nil
|
||||||
|
} else {
|
||||||
|
vc.listeningForHotkeyView!.updateShortcut(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
44
phpmon/Domain/Preferences/Views/CheckboxPreferenceView.swift
Normal 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
67
phpmon/Domain/Preferences/Views/CheckboxPreferenceView.xib
Normal 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>
|
97
phpmon/Domain/Preferences/Views/HotkeyPreferenceView.swift
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
84
phpmon/Domain/Preferences/Views/HotkeyPreferenceView.xib
Normal 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>
|
59
phpmon/Domain/SiteList/SiteListCell.swift
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
//
|
||||||
|
// 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!
|
||||||
|
|
||||||
|
@IBOutlet weak var buttonWarning: NSButton!
|
||||||
|
@IBOutlet weak var labelWarning: 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)"
|
||||||
|
|
||||||
|
let isProblematic = site.name.contains(" ")
|
||||||
|
buttonWarning.isHidden = !isProblematic
|
||||||
|
labelWarning.isHidden = !isProblematic
|
||||||
|
labelWarning.stringValue = "site_list.warning.spaces".localized
|
||||||
|
|
||||||
|
// Show the absolute path, except make sure to replace the /Users/username segment with ~ for readability
|
||||||
|
labelPathName.stringValue = site.absolutePath
|
||||||
|
.replacingOccurrences(of: "/Users/\(Paths.whoami)", with: "~")
|
||||||
|
|
||||||
|
// 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 ?? "???"
|
||||||
|
}
|
||||||
|
}
|
97
phpmon/Domain/SiteList/SiteListVC+Actions.swift
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
//
|
||||||
|
// SiteListVC+Actions.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 23/12/2021.
|
||||||
|
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Cocoa
|
||||||
|
|
||||||
|
extension SiteListVC {
|
||||||
|
|
||||||
|
@objc 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func openInBrowser() {
|
||||||
|
let prefix = selectedSite!.secured ? "https://" : "http://"
|
||||||
|
let url = URL(string: "\(prefix)\(selectedSite!.name!).\(Valet.shared.config.tld)")
|
||||||
|
if url != nil {
|
||||||
|
NSWorkspace.shared.open(url!)
|
||||||
|
} else {
|
||||||
|
_ = Alert.present(
|
||||||
|
messageText: "site_list.alert.invalid_folder_name".localized,
|
||||||
|
informativeText: "site_list.alert.invalid_folder_name_desc".localized
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func openInFinder() {
|
||||||
|
Shell.run("open '\(selectedSite!.absolutePath!)'")
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func openInTerminal() {
|
||||||
|
Shell.run("open -b com.apple.terminal '\(selectedSite!.absolutePath!)'")
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func openWithEditor(sender: EditorMenuItem) {
|
||||||
|
guard let editor = sender.editor else { return }
|
||||||
|
editor.openDirectory(file: selectedSite!.absolutePath!)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc 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()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
93
phpmon/Domain/SiteList/SiteListVC+ContextMenu.swift
Normal 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())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
191
phpmon/Domain/SiteList/SiteListVC.swift
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
//
|
||||||
|
// 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.
|
||||||
|
*/
|
||||||
|
internal 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: - (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: - Deinitialization
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
print("VC deallocated")
|
||||||
|
}
|
||||||
|
}
|