mirror of
https://github.com/nicoverbruggen/phpmon.git
synced 2026-03-25 21:50:08 +01:00
✨ Add Command History to menu
This commit is contained in:
@@ -209,6 +209,9 @@
|
||||
03FE39E72E81682800B7B5AC /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 03FE39E52E81682800B7B5AC /* AppIcon.icon */; };
|
||||
03FE39E82E81682800B7B5AC /* AppIconEAP.icon in Resources */ = {isa = PBXBuildFile; fileRef = 03FE39E62E81682800B7B5AC /* AppIconEAP.icon */; };
|
||||
03FE39EA2E81694500B7B5AC /* AppIconUD.icon in Resources */ = {isa = PBXBuildFile; fileRef = 03FE39E92E81694500B7B5AC /* AppIconUD.icon */; };
|
||||
0A1A6208D3DD2495FBD8569B /* CommandHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31503E15DADA980998F0F5A2 /* CommandHistoryView.swift */; };
|
||||
25250C95E34D20ED4C91C320 /* CommandHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31503E15DADA980998F0F5A2 /* CommandHistoryView.swift */; };
|
||||
4181B8F1C0ED930B2C5E5532 /* CommandHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31503E15DADA980998F0F5A2 /* CommandHistoryView.swift */; };
|
||||
5420395926135DC100FB00FA /* PreferencesVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395826135DC100FB00FA /* PreferencesVC.swift */; };
|
||||
5420395F2613607600FB00FA /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395E2613607600FB00FA /* Preferences.swift */; };
|
||||
5489625828312FAD004F647A /* CreatedFromFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5489625728312FAD004F647A /* CreatedFromFile.swift */; };
|
||||
@@ -216,6 +219,7 @@
|
||||
54A18D40282A566E000A0D81 /* nginx-secure-proxy-custom-tld.test in Resources */ = {isa = PBXBuildFile; fileRef = 54A18D3F282A566E000A0D81 /* nginx-secure-proxy-custom-tld.test */; };
|
||||
54B48B5F275F66AE006D90C5 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B48B5E275F66AE006D90C5 /* Application.swift */; };
|
||||
54B48B60275F66AE006D90C5 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B48B5E275F66AE006D90C5 /* Application.swift */; };
|
||||
54D6418DBC6AF23FED489018 /* CommandHistoryWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51118AFDD02AA1B7D8C9278 /* CommandHistoryWindowController.swift */; };
|
||||
54D9E0B227E4F51E003B9AD9 /* HotKeysController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D9E0AC27E4F51E003B9AD9 /* HotKeysController.swift */; };
|
||||
54D9E0B327E4F51E003B9AD9 /* HotKeysController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D9E0AC27E4F51E003B9AD9 /* HotKeysController.swift */; };
|
||||
54D9E0B427E4F51E003B9AD9 /* Key.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D9E0AD27E4F51E003B9AD9 /* Key.swift */; };
|
||||
@@ -235,6 +239,9 @@
|
||||
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 */; };
|
||||
5FD883E3DBC963F53E12A17F /* CommandHistoryWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51118AFDD02AA1B7D8C9278 /* CommandHistoryWindowController.swift */; };
|
||||
719E6363E4F41C8A599FCC99 /* CommandHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31503E15DADA980998F0F5A2 /* CommandHistoryView.swift */; };
|
||||
BB3AF45BED2C82AE27E86107 /* CommandHistoryWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51118AFDD02AA1B7D8C9278 /* CommandHistoryWindowController.swift */; };
|
||||
C40175B82903108900763A68 /* ValetInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40175B72903108900763A68 /* ValetInteractor.swift */; };
|
||||
C40175B92903108900763A68 /* ValetInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40175B72903108900763A68 /* ValetInteractor.swift */; };
|
||||
C40175BA2903108900763A68 /* ValetInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40175B72903108900763A68 /* ValetInteractor.swift */; };
|
||||
@@ -321,7 +328,6 @@
|
||||
C42106682AFA9FF400DF3732 /* PhpVersionManagerView+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42106652AFA9FF400DF3732 /* PhpVersionManagerView+Actions.swift */; };
|
||||
C42106692AFA9FF400DF3732 /* PhpVersionManagerView+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42106652AFA9FF400DF3732 /* PhpVersionManagerView+Actions.swift */; };
|
||||
C422DDAA28A2C49900CEAC97 /* PhpDoctorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C422DDA928A2C49900CEAC97 /* PhpDoctorView.swift */; };
|
||||
719E6363E4F41C8A599FCC99 /* ActiveCommandsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31503E15DADA980998F0F5A2 /* ActiveCommandsView.swift */; };
|
||||
C4232EE52612526500158FC6 /* Credits.html in Resources */ = {isa = PBXBuildFile; fileRef = C4232EE42612526500158FC6 /* Credits.html */; };
|
||||
C42337A3281F19F000459A48 /* Xdebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42337A2281F19F000459A48 /* Xdebug.swift */; };
|
||||
C42759672627662800093CAE /* NSMenuExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42759662627662800093CAE /* NSMenuExtension.swift */; };
|
||||
@@ -637,7 +643,6 @@
|
||||
C471E86428F9BB650021E251 /* Warning.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47699F028A2F3150060FEB8 /* Warning.swift */; };
|
||||
C471E86528F9BB650021E251 /* WarningManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47699EE28A2F2A30060FEB8 /* WarningManager.swift */; };
|
||||
C471E86628F9BB650021E251 /* PhpDoctorWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C422DDAC28A2DAC600CEAC97 /* PhpDoctorWindowController.swift */; };
|
||||
DE164E569298309E4224C5D6 /* ActiveCommandsWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51118AFDD02AA1B7D8C9278 /* ActiveCommandsWindowController.swift */; };
|
||||
C471E86728F9BB650021E251 /* OnboardingWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4FACE82288F1F9700FC478F /* OnboardingWindowController.swift */; };
|
||||
C471E86828F9BB650021E251 /* PreferencesWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4998F092617633900B2526E /* PreferencesWindowController.swift */; };
|
||||
C471E86928F9BB650021E251 /* PreferencesWindowController+Hotkey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4FACE7F288F1C0D00FC478F /* PreferencesWindowController+Hotkey.swift */; };
|
||||
@@ -658,7 +663,6 @@
|
||||
C471E87E28F9BB650021E251 /* PresetHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C463E37F284930EE00422731 /* PresetHelper.swift */; };
|
||||
C471E87F28F9BB650021E251 /* WarningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4297F7928970D59004C4630 /* WarningView.swift */; };
|
||||
C471E88028F9BB650021E251 /* PhpDoctorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C422DDA928A2C49900CEAC97 /* PhpDoctorView.swift */; };
|
||||
25250C95E34D20ED4C91C320 /* ActiveCommandsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31503E15DADA980998F0F5A2 /* ActiveCommandsView.swift */; };
|
||||
C471E88128F9BB650021E251 /* NoWarningsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C708C28AA7F7900E8D498 /* NoWarningsView.swift */; };
|
||||
C471E88228F9BB650021E251 /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E9D2BF2878B336008FFDAD /* OnboardingView.swift */; };
|
||||
C471E88328F9BB650021E251 /* VersionPopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44264BF2850BD2A007400F1 /* VersionPopoverView.swift */; };
|
||||
@@ -720,7 +724,6 @@
|
||||
C471E8C728F9BB8F0021E251 /* Warning.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47699F028A2F3150060FEB8 /* Warning.swift */; };
|
||||
C471E8C828F9BB8F0021E251 /* WarningManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47699EE28A2F2A30060FEB8 /* WarningManager.swift */; };
|
||||
C471E8C928F9BB8F0021E251 /* PhpDoctorWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C422DDAC28A2DAC600CEAC97 /* PhpDoctorWindowController.swift */; };
|
||||
BB3AF45BED2C82AE27E86107 /* ActiveCommandsWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51118AFDD02AA1B7D8C9278 /* ActiveCommandsWindowController.swift */; };
|
||||
C471E8CA28F9BB8F0021E251 /* OnboardingWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4FACE82288F1F9700FC478F /* OnboardingWindowController.swift */; };
|
||||
C471E8CB28F9BB8F0021E251 /* PreferencesWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4998F092617633900B2526E /* PreferencesWindowController.swift */; };
|
||||
C471E8CC28F9BB8F0021E251 /* PreferencesWindowController+Hotkey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4FACE7F288F1C0D00FC478F /* PreferencesWindowController+Hotkey.swift */; };
|
||||
@@ -741,7 +744,6 @@
|
||||
C471E8E128F9BB8F0021E251 /* PresetHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C463E37F284930EE00422731 /* PresetHelper.swift */; };
|
||||
C471E8E228F9BB8F0021E251 /* WarningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4297F7928970D59004C4630 /* WarningView.swift */; };
|
||||
C471E8E328F9BB8F0021E251 /* PhpDoctorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C422DDA928A2C49900CEAC97 /* PhpDoctorView.swift */; };
|
||||
0A1A6208D3DD2495FBD8569B /* ActiveCommandsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31503E15DADA980998F0F5A2 /* ActiveCommandsView.swift */; };
|
||||
C471E8E428F9BB8F0021E251 /* NoWarningsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C708C28AA7F7900E8D498 /* NoWarningsView.swift */; };
|
||||
C471E8E528F9BB8F0021E251 /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E9D2BF2878B336008FFDAD /* OnboardingView.swift */; };
|
||||
C471E8E628F9BB8F0021E251 /* VersionPopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44264BF2850BD2A007400F1 /* VersionPopoverView.swift */; };
|
||||
@@ -775,9 +777,7 @@
|
||||
C485706D28BF450900539B36 /* NSMenuItemExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40508B028ADAB44008FAC1F /* NSMenuItemExtension.swift */; };
|
||||
C485706E28BF451C00539B36 /* OnboardingWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4FACE82288F1F9700FC478F /* OnboardingWindowController.swift */; };
|
||||
C485706F28BF452300539B36 /* PhpDoctorWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C422DDAC28A2DAC600CEAC97 /* PhpDoctorWindowController.swift */; };
|
||||
54D6418DBC6AF23FED489018 /* ActiveCommandsWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51118AFDD02AA1B7D8C9278 /* ActiveCommandsWindowController.swift */; };
|
||||
C485707028BF452300539B36 /* PhpDoctorWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C422DDAC28A2DAC600CEAC97 /* PhpDoctorWindowController.swift */; };
|
||||
5FD883E3DBC963F53E12A17F /* ActiveCommandsWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51118AFDD02AA1B7D8C9278 /* ActiveCommandsWindowController.swift */; };
|
||||
C485707128BF452E00539B36 /* WarningManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47699EE28A2F2A30060FEB8 /* WarningManager.swift */; };
|
||||
C485707228BF453800539B36 /* SwiftUIHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44264BD2850B86C007400F1 /* SwiftUIHelper.swift */; };
|
||||
C485707328BF454300539B36 /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E9D2BF2878B336008FFDAD /* OnboardingView.swift */; };
|
||||
@@ -788,7 +788,6 @@
|
||||
C485707828BF456300539B36 /* Warning.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47699F028A2F3150060FEB8 /* Warning.swift */; };
|
||||
C485707928BF456C00539B36 /* ArrayExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EB53E628553117006F9937 /* ArrayExtension.swift */; };
|
||||
C485707A28BF457800539B36 /* PhpDoctorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C422DDA928A2C49900CEAC97 /* PhpDoctorView.swift */; };
|
||||
4181B8F1C0ED930B2C5E5532 /* ActiveCommandsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31503E15DADA980998F0F5A2 /* ActiveCommandsView.swift */; };
|
||||
C485707B28BF458900539B36 /* VersionPopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44264BF2850BD2A007400F1 /* VersionPopoverView.swift */; };
|
||||
C485707C28BF459500539B36 /* NoWarningsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C708C28AA7F7900E8D498 /* NoWarningsView.swift */; };
|
||||
C485707D28BF45A200539B36 /* WarningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4297F7928970D59004C4630 /* WarningView.swift */; };
|
||||
@@ -1030,6 +1029,7 @@
|
||||
C4FD87A829AB9ABD0002D701 /* PhpConfigChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43FDBE829A932B0003D85EC /* PhpConfigChecker.swift */; };
|
||||
C4FD87A929AB9ABD0002D701 /* PhpConfigChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43FDBE829A932B0003D85EC /* PhpConfigChecker.swift */; };
|
||||
C4FD87AA29AB9ABD0002D701 /* PhpConfigChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43FDBE829A932B0003D85EC /* PhpConfigChecker.swift */; };
|
||||
DE164E569298309E4224C5D6 /* CommandHistoryWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51118AFDD02AA1B7D8C9278 /* CommandHistoryWindowController.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -1131,6 +1131,7 @@
|
||||
03FE39E52E81682800B7B5AC /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIcon.icon; sourceTree = "<group>"; };
|
||||
03FE39E62E81682800B7B5AC /* AppIconEAP.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIconEAP.icon; sourceTree = "<group>"; };
|
||||
03FE39E92E81694500B7B5AC /* AppIconUD.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIconUD.icon; sourceTree = "<group>"; };
|
||||
31503E15DADA980998F0F5A2 /* CommandHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandHistoryView.swift; sourceTree = "<group>"; };
|
||||
5420395826135DC100FB00FA /* PreferencesVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesVC.swift; sourceTree = "<group>"; };
|
||||
5420395E2613607600FB00FA /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
|
||||
5489625728312FAD004F647A /* CreatedFromFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatedFromFile.swift; sourceTree = "<group>"; };
|
||||
@@ -1198,8 +1199,6 @@
|
||||
C42106652AFA9FF400DF3732 /* PhpVersionManagerView+Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PhpVersionManagerView+Actions.swift"; sourceTree = "<group>"; };
|
||||
C422DDA928A2C49900CEAC97 /* PhpDoctorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpDoctorView.swift; sourceTree = "<group>"; };
|
||||
C422DDAC28A2DAC600CEAC97 /* PhpDoctorWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpDoctorWindowController.swift; sourceTree = "<group>"; };
|
||||
E51118AFDD02AA1B7D8C9278 /* ActiveCommandsWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveCommandsWindowController.swift; sourceTree = "<group>"; };
|
||||
31503E15DADA980998F0F5A2 /* ActiveCommandsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveCommandsView.swift; sourceTree = "<group>"; };
|
||||
C4232EE42612526500158FC6 /* Credits.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = Credits.html; sourceTree = "<group>"; };
|
||||
C42337A2281F19F000459A48 /* Xdebug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Xdebug.swift; sourceTree = "<group>"; };
|
||||
C42759662627662800093CAE /* NSMenuExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSMenuExtension.swift; sourceTree = "<group>"; };
|
||||
@@ -1393,6 +1392,7 @@
|
||||
C4FC8D412A49816300FBBD16 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
C4FC8D432A49816C00FBBD16 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
C4FC8D442A4981BC00FBBD16 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
E51118AFDD02AA1B7D8C9278 /* CommandHistoryWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandHistoryWindowController.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -1725,7 +1725,6 @@
|
||||
C40C7F2F27722E8D00DDDCDC /* Logger.swift */,
|
||||
C417DC73277614690015E6EE /* Helpers.swift */,
|
||||
C4CB6E64292C362C002E9027 /* Homebrew.swift */,
|
||||
031A80DB2F4CF1690016F7DD /* CommandTracker.swift */,
|
||||
);
|
||||
path = Core;
|
||||
sourceTree = "<group>";
|
||||
@@ -1975,23 +1974,6 @@
|
||||
path = Data;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F0F42C1C96504D46C75AD6F8 /* UI */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E51118AFDD02AA1B7D8C9278 /* ActiveCommandsWindowController.swift */,
|
||||
31503E15DADA980998F0F5A2 /* ActiveCommandsView.swift */,
|
||||
);
|
||||
path = UI;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E1E4FBD76E71C469A65AC4F8 /* Active Commands */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F0F42C1C96504D46C75AD6F8 /* UI */,
|
||||
);
|
||||
path = "Active Commands";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C44DFA832A6706A200B98ED5 /* Modules */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -1999,7 +1981,7 @@
|
||||
C464ADAA275A7A25003FCD53 /* Domain List */,
|
||||
C44DFA7A2A6703FD00B98ED5 /* PHP Config Editor */,
|
||||
C4297F7828970D4E004C4630 /* PHP Doctor */,
|
||||
E1E4FBD76E71C469A65AC4F8 /* Active Commands */,
|
||||
E1E4FBD76E71C469A65AC4F8 /* Command History */,
|
||||
C43931C329C4BD510069165B /* PHP Version Manager */,
|
||||
C4292D512B023F37004F0D2A /* PHP Extension Manager */,
|
||||
);
|
||||
@@ -2470,6 +2452,7 @@
|
||||
C4B5853D2770FE3900DA4FBE /* RealCommand.swift */,
|
||||
C4E49DEC28F764A00026AC4E /* TestableCommand.swift */,
|
||||
C4E49DE928F7643D0026AC4E /* CommandProtocol.swift */,
|
||||
031A80DB2F4CF1690016F7DD /* CommandTracker.swift */,
|
||||
);
|
||||
path = Command;
|
||||
sourceTree = "<group>";
|
||||
@@ -2566,6 +2549,23 @@
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E1E4FBD76E71C469A65AC4F8 /* Command History */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F0F42C1C96504D46C75AD6F8 /* UI */,
|
||||
);
|
||||
path = "Command History";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F0F42C1C96504D46C75AD6F8 /* UI */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E51118AFDD02AA1B7D8C9278 /* CommandHistoryWindowController.swift */,
|
||||
31503E15DADA980998F0F5A2 /* CommandHistoryView.swift */,
|
||||
);
|
||||
path = UI;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@@ -3024,7 +3024,7 @@
|
||||
03D846352EB64E39006EFE3C /* CrashReporter.swift in Sources */,
|
||||
C42759672627662800093CAE /* NSMenuExtension.swift in Sources */,
|
||||
C422DDAA28A2C49900CEAC97 /* PhpDoctorView.swift in Sources */,
|
||||
719E6363E4F41C8A599FCC99 /* ActiveCommandsView.swift in Sources */,
|
||||
719E6363E4F41C8A599FCC99 /* CommandHistoryView.swift in Sources */,
|
||||
031A80DE2F4CF1690016F7DD /* CommandTracker.swift in Sources */,
|
||||
C469E6FE294CF7B200A82AB2 /* FakeValetProxy.swift in Sources */,
|
||||
C464ADAF275A7A69003FCD53 /* DomainListVC.swift in Sources */,
|
||||
@@ -3063,7 +3063,7 @@
|
||||
031D74842F46225C00D4FF48 /* SelectDomainTypeView.swift in Sources */,
|
||||
C4D5CFCA27E0F9CD00035329 /* NginxConfigurationFile.swift in Sources */,
|
||||
C485707028BF452300539B36 /* PhpDoctorWindowController.swift in Sources */,
|
||||
5FD883E3DBC963F53E12A17F /* ActiveCommandsWindowController.swift in Sources */,
|
||||
5FD883E3DBC963F53E12A17F /* CommandHistoryWindowController.swift in Sources */,
|
||||
C4CE3BBA27B31F670086CA49 /* ComposerWindow.swift in Sources */,
|
||||
C40D725A2A018ACC0054A067 /* BusyStatus.swift in Sources */,
|
||||
031F24802EA1071A00CFB8D9 /* Container+Fake.swift in Sources */,
|
||||
@@ -3203,7 +3203,7 @@
|
||||
C45B9150295608E300F4EC78 /* ValetServicesManager.swift in Sources */,
|
||||
C471E86528F9BB650021E251 /* WarningManager.swift in Sources */,
|
||||
C471E86628F9BB650021E251 /* PhpDoctorWindowController.swift in Sources */,
|
||||
DE164E569298309E4224C5D6 /* ActiveCommandsWindowController.swift in Sources */,
|
||||
DE164E569298309E4224C5D6 /* CommandHistoryWindowController.swift in Sources */,
|
||||
C471E86728F9BB650021E251 /* OnboardingWindowController.swift in Sources */,
|
||||
C471E86828F9BB650021E251 /* PreferencesWindowController.swift in Sources */,
|
||||
C471E86928F9BB650021E251 /* PreferencesWindowController+Hotkey.swift in Sources */,
|
||||
@@ -3236,7 +3236,7 @@
|
||||
C471E87E28F9BB650021E251 /* PresetHelper.swift in Sources */,
|
||||
C471E87F28F9BB650021E251 /* WarningView.swift in Sources */,
|
||||
C471E88028F9BB650021E251 /* PhpDoctorView.swift in Sources */,
|
||||
25250C95E34D20ED4C91C320 /* ActiveCommandsView.swift in Sources */,
|
||||
25250C95E34D20ED4C91C320 /* CommandHistoryView.swift in Sources */,
|
||||
C471E88128F9BB650021E251 /* NoWarningsView.swift in Sources */,
|
||||
C471E88228F9BB650021E251 /* OnboardingView.swift in Sources */,
|
||||
C471E88328F9BB650021E251 /* VersionPopoverView.swift in Sources */,
|
||||
@@ -3440,7 +3440,7 @@
|
||||
C471E8C828F9BB8F0021E251 /* WarningManager.swift in Sources */,
|
||||
C46DC7A72C7B5BCA00F19D17 /* Favorites.swift in Sources */,
|
||||
C471E8C928F9BB8F0021E251 /* PhpDoctorWindowController.swift in Sources */,
|
||||
BB3AF45BED2C82AE27E86107 /* ActiveCommandsWindowController.swift in Sources */,
|
||||
BB3AF45BED2C82AE27E86107 /* CommandHistoryWindowController.swift in Sources */,
|
||||
C41ADCEB2970CCC700120423 /* FSNotifier.swift in Sources */,
|
||||
C471E8CA28F9BB8F0021E251 /* OnboardingWindowController.swift in Sources */,
|
||||
C456A0C92AA614BD0080144F /* PhpPreference.swift in Sources */,
|
||||
@@ -3479,7 +3479,7 @@
|
||||
031D747D2F46225600D4FF48 /* SimpleButton.swift in Sources */,
|
||||
C471E8E228F9BB8F0021E251 /* WarningView.swift in Sources */,
|
||||
C471E8E328F9BB8F0021E251 /* PhpDoctorView.swift in Sources */,
|
||||
0A1A6208D3DD2495FBD8569B /* ActiveCommandsView.swift in Sources */,
|
||||
0A1A6208D3DD2495FBD8569B /* CommandHistoryView.swift in Sources */,
|
||||
C471E8E428F9BB8F0021E251 /* NoWarningsView.swift in Sources */,
|
||||
C471E8E528F9BB8F0021E251 /* OnboardingView.swift in Sources */,
|
||||
C4B79EBF29CA38DB00A483EE /* BrewCommand.swift in Sources */,
|
||||
@@ -3641,7 +3641,7 @@
|
||||
C41ADCE92970CCC700120423 /* FSNotifier.swift in Sources */,
|
||||
C40C7F2927721FF600DDDCDC /* Valet+Alerts.swift in Sources */,
|
||||
C485707A28BF457800539B36 /* PhpDoctorView.swift in Sources */,
|
||||
4181B8F1C0ED930B2C5E5532 /* ActiveCommandsView.swift in Sources */,
|
||||
4181B8F1C0ED930B2C5E5532 /* CommandHistoryView.swift in Sources */,
|
||||
C4C0E8E827F88B41002D32A9 /* DomainScanner.swift in Sources */,
|
||||
C449B4F027EE7FB800C47E8A /* DomainListTLSCell.swift in Sources */,
|
||||
037F44162EDB0AAA002EBF75 /* FSNotifierTest.swift in Sources */,
|
||||
@@ -3826,7 +3826,7 @@
|
||||
C449B4F327EE7FC600C47E8A /* DomainListTypeCell.swift in Sources */,
|
||||
C48D6C71279CD2AC00F26D7E /* VersionNumber.swift in Sources */,
|
||||
C485706F28BF452300539B36 /* PhpDoctorWindowController.swift in Sources */,
|
||||
54D6418DBC6AF23FED489018 /* ActiveCommandsWindowController.swift in Sources */,
|
||||
54D6418DBC6AF23FED489018 /* CommandHistoryWindowController.swift in Sources */,
|
||||
C46FA9892822EFDC00D78807 /* PhpConfigurationFile.swift in Sources */,
|
||||
C43931CB29C4C03F0069165B /* Brew.swift in Sources */,
|
||||
0392CDEC2EB25371009176DA /* SecurePopoverView.swift in Sources */,
|
||||
|
||||
@@ -9,6 +9,20 @@
|
||||
import Foundation
|
||||
|
||||
protocol CommandProtocol {
|
||||
/**
|
||||
Immediately executes a command, without tracking.
|
||||
|
||||
- Parameter path: The path of the command or program to invoke.
|
||||
- Parameter arguments: A list of arguments that are passed on.
|
||||
- Parameter trimNewlines: Removes empty new line output.
|
||||
- Parameter withStandardError: Outputs standard error output to the same string output as well.
|
||||
*/
|
||||
func executeRaw(
|
||||
path: String,
|
||||
arguments: [String],
|
||||
trimNewlines: Bool,
|
||||
withStandardError: Bool
|
||||
) -> String
|
||||
|
||||
/**
|
||||
Immediately executes a command.
|
||||
@@ -39,3 +53,54 @@ protocol CommandProtocol {
|
||||
) -> String
|
||||
|
||||
}
|
||||
|
||||
extension CommandProtocol {
|
||||
func execute(
|
||||
path: String,
|
||||
arguments: [String],
|
||||
trimNewlines: Bool,
|
||||
withStandardError: Bool
|
||||
) -> String {
|
||||
executeRaw(
|
||||
path: path,
|
||||
arguments: arguments,
|
||||
trimNewlines: trimNewlines,
|
||||
withStandardError: withStandardError
|
||||
)
|
||||
}
|
||||
|
||||
func execute(
|
||||
path: String,
|
||||
arguments: [String],
|
||||
trimNewlines: Bool
|
||||
) -> String {
|
||||
execute(
|
||||
path: path,
|
||||
arguments: arguments,
|
||||
trimNewlines: trimNewlines,
|
||||
withStandardError: false
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protocol TrackedCommandProtocol: CommandProtocol, CommandTrackingProvider {}
|
||||
|
||||
extension TrackedCommandProtocol {
|
||||
func execute(
|
||||
path: String,
|
||||
arguments: [String],
|
||||
trimNewlines: Bool,
|
||||
withStandardError: Bool
|
||||
) -> String {
|
||||
let commandDescription = "\(path) \(arguments.joined(separator: " "))"
|
||||
return trackedCommand(description: commandDescription) {
|
||||
executeRaw(
|
||||
path: path,
|
||||
arguments: arguments,
|
||||
trimNewlines: trimNewlines,
|
||||
withStandardError: withStandardError
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
112
phpmon/Common/Command/CommandTracker.swift
Normal file
112
phpmon/Common/Command/CommandTracker.swift
Normal file
@@ -0,0 +1,112 @@
|
||||
//
|
||||
// CommandTracker.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Copyright © 2025 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
@preconcurrency import Dispatch
|
||||
|
||||
@MainActor
|
||||
class CommandTracker: ObservableObject {
|
||||
nonisolated init() {}
|
||||
|
||||
private let maxStoredCommands = 200
|
||||
@Published private(set) var commands: [TrackedCommand] = []
|
||||
|
||||
var activeCommands: [TrackedCommand] {
|
||||
commands.filter { !$0.isCompleted }
|
||||
}
|
||||
|
||||
var isActive: Bool {
|
||||
!activeCommands.isEmpty
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func track(_ command: String, id: UUID = UUID()) -> UUID {
|
||||
let tracked = TrackedCommand(id: id, command: command, startedAt: Date())
|
||||
commands.append(tracked)
|
||||
if commands.count > maxStoredCommands {
|
||||
commands.removeFirst(commands.count - maxStoredCommands)
|
||||
}
|
||||
return tracked.id
|
||||
}
|
||||
|
||||
func complete(_ id: UUID) {
|
||||
if let index = commands.firstIndex(where: { $0.id == id }) {
|
||||
commands[index].completedAt = Date()
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated func trackFromAnyThread(_ command: String) -> UUID {
|
||||
let id = UUID()
|
||||
Task { @MainActor in
|
||||
self.track(command, id: id)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
nonisolated func completeFromAnyThread(_ id: UUID) {
|
||||
Task { @MainActor in
|
||||
self.complete(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Tracked Command
|
||||
|
||||
struct TrackedCommand: Identifiable {
|
||||
let id: UUID
|
||||
let command: String
|
||||
let startedAt: Date
|
||||
var completedAt: Date?
|
||||
|
||||
var isCompleted: Bool {
|
||||
completedAt != nil
|
||||
}
|
||||
|
||||
func durationText(at date: Date = Date()) -> String {
|
||||
if let completedAt {
|
||||
let duration = completedAt.timeIntervalSince(startedAt)
|
||||
|
||||
if duration < 0.001 {
|
||||
let micros = max(1, Int(duration * 1_000_000))
|
||||
return "Completed in \(micros) μs"
|
||||
}
|
||||
|
||||
let ms = max(1, Int(duration * 1000))
|
||||
return "Completed in \(ms) ms"
|
||||
}
|
||||
|
||||
let ms = max(1, Int(date.timeIntervalSince(startedAt) * 1000))
|
||||
return "Running for \(ms) ms"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Command Tracking
|
||||
|
||||
protocol CommandTrackingProvider {
|
||||
var commandTracker: CommandTracker { get }
|
||||
}
|
||||
|
||||
extension CommandTrackingProvider {
|
||||
func trackedCommand<T>(description: String, _ work: () -> T) -> T {
|
||||
let trackingId = commandTracker.trackFromAnyThread(description)
|
||||
defer {
|
||||
commandTracker.completeFromAnyThread(trackingId)
|
||||
}
|
||||
return work()
|
||||
}
|
||||
|
||||
func trackedCommandAsync<T>(
|
||||
description: String,
|
||||
_ work: () async throws -> T
|
||||
) async rethrows -> T {
|
||||
let trackingId = commandTracker.trackFromAnyThread(description)
|
||||
defer {
|
||||
commandTracker.completeFromAnyThread(trackingId)
|
||||
}
|
||||
return try await work()
|
||||
}
|
||||
}
|
||||
@@ -7,29 +7,19 @@
|
||||
|
||||
import Cocoa
|
||||
|
||||
public class RealCommand: CommandProtocol {
|
||||
private let commandTracker: CommandTracker
|
||||
public class RealCommand: TrackedCommandProtocol {
|
||||
let commandTracker: CommandTracker
|
||||
|
||||
init(commandTracker: CommandTracker) {
|
||||
self.commandTracker = commandTracker
|
||||
}
|
||||
|
||||
public func execute(
|
||||
public func executeRaw(
|
||||
path: String,
|
||||
arguments: [String],
|
||||
trimNewlines: Bool,
|
||||
withStandardError: Bool
|
||||
) -> String {
|
||||
let tracker = self.commandTracker
|
||||
let commandDescription = "\(path) \(arguments.joined(separator: " "))"
|
||||
var trackingId: UUID?
|
||||
DispatchQueue.main.async { trackingId = tracker.track(commandDescription) }
|
||||
defer {
|
||||
DispatchQueue.main.async {
|
||||
if let id = trackingId { tracker.complete(id) }
|
||||
}
|
||||
}
|
||||
|
||||
let task = Process()
|
||||
var output = ""
|
||||
|
||||
|
||||
@@ -15,15 +15,12 @@ class TestableCommand: CommandProtocol {
|
||||
|
||||
var commands: [String: String]
|
||||
|
||||
func execute(path: String, arguments: [String]) -> String {
|
||||
self.execute(path: path, arguments: arguments, trimNewlines: false)
|
||||
}
|
||||
|
||||
public func execute(path: String, arguments: [String], trimNewlines: Bool, withStandardError: Bool) -> String {
|
||||
self.execute(path: path, arguments: arguments, trimNewlines: trimNewlines)
|
||||
}
|
||||
|
||||
public func execute(path: String, arguments: [String], trimNewlines: Bool) -> String {
|
||||
public func executeRaw(
|
||||
path: String,
|
||||
arguments: [String],
|
||||
trimNewlines: Bool,
|
||||
withStandardError: Bool
|
||||
) -> String {
|
||||
let concatenatedCommand = "\(path) \(arguments.joined(separator: " "))"
|
||||
assert(commands.keys.contains(concatenatedCommand), "Command `\(concatenatedCommand)` not found")
|
||||
return self.commands[concatenatedCommand]!
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
//
|
||||
// CommandTracker.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Copyright © 2025 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct TrackedCommand: Identifiable {
|
||||
let id: UUID
|
||||
let command: String
|
||||
let startedAt: Date
|
||||
var completedAt: Date?
|
||||
|
||||
var isCompleted: Bool {
|
||||
completedAt != nil
|
||||
}
|
||||
|
||||
var durationText: String {
|
||||
let end = completedAt ?? Date()
|
||||
let ms = Int(end.timeIntervalSince(startedAt) * 1000)
|
||||
if isCompleted {
|
||||
return "Completed in \(ms) ms"
|
||||
} else {
|
||||
return "Running for \(ms) ms"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
class CommandTracker: ObservableObject {
|
||||
nonisolated init() {}
|
||||
|
||||
@Published private(set) var commands: [TrackedCommand] = []
|
||||
|
||||
var activeCommands: [TrackedCommand] {
|
||||
commands.filter { !$0.isCompleted }
|
||||
}
|
||||
|
||||
var isActive: Bool {
|
||||
!activeCommands.isEmpty
|
||||
}
|
||||
|
||||
var loggingEnabled: Bool = true
|
||||
|
||||
@discardableResult
|
||||
func track(_ command: String) -> UUID {
|
||||
let tracked = TrackedCommand(id: UUID(), command: command, startedAt: Date())
|
||||
commands.append(tracked)
|
||||
if loggingEnabled {
|
||||
logActiveCommands("TRACK")
|
||||
}
|
||||
return tracked.id
|
||||
}
|
||||
|
||||
func complete(_ id: UUID) {
|
||||
if let index = commands.firstIndex(where: { $0.id == id }) {
|
||||
commands[index].completedAt = Date()
|
||||
}
|
||||
if loggingEnabled {
|
||||
logActiveCommands("COMPLETE")
|
||||
}
|
||||
}
|
||||
|
||||
private func logActiveCommands(_ label: String) {
|
||||
if activeCommands.isEmpty {
|
||||
Log.info("[CommandTracker] [\(label)] No active commands.")
|
||||
} else {
|
||||
let list = activeCommands
|
||||
.map { " - \($0.command) (started: \($0.startedAt))" }
|
||||
.joined(separator: "\n")
|
||||
Log.info("[CommandTracker] [\(label)] Active commands:\n\(list)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
@preconcurrency import Dispatch
|
||||
|
||||
class RealShell: ShellProtocol, @unchecked Sendable {
|
||||
class RealShell: TrackedShellProtocol, @unchecked Sendable {
|
||||
init(binPath: String, commandTracker: CommandTracker) {
|
||||
self.binPath = binPath
|
||||
self.commandTracker = commandTracker
|
||||
@@ -17,7 +17,7 @@ class RealShell: ShellProtocol, @unchecked Sendable {
|
||||
self._exports = [:]
|
||||
}
|
||||
|
||||
private let commandTracker: CommandTracker
|
||||
let commandTracker: CommandTracker
|
||||
private(set) var binPath: String
|
||||
|
||||
/**
|
||||
@@ -157,16 +157,7 @@ class RealShell: ShellProtocol, @unchecked Sendable {
|
||||
|
||||
// MARK: - Shellable Protocol
|
||||
|
||||
func sync(_ command: String) -> ShellOutput {
|
||||
let tracker = self.commandTracker
|
||||
var trackingId: UUID?
|
||||
DispatchQueue.main.async { trackingId = tracker.track(command) }
|
||||
defer {
|
||||
DispatchQueue.main.async {
|
||||
if let id = trackingId { tracker.complete(id) }
|
||||
}
|
||||
}
|
||||
|
||||
func syncRaw(_ command: String) -> ShellOutput {
|
||||
let process = getShellProcess(for: command)
|
||||
|
||||
let outputPipe = Pipe()
|
||||
@@ -197,13 +188,7 @@ class RealShell: ShellProtocol, @unchecked Sendable {
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func pipe(_ command: String) async -> ShellOutput {
|
||||
let trackingId = await commandTracker.track(command)
|
||||
defer {
|
||||
let tracker = self.commandTracker
|
||||
DispatchQueue.main.async { tracker.complete(trackingId) }
|
||||
}
|
||||
|
||||
func pipeRaw(_ command: String) async -> ShellOutput {
|
||||
let process = getShellProcess(for: command)
|
||||
|
||||
let outputPipe = Pipe()
|
||||
@@ -239,13 +224,7 @@ class RealShell: ShellProtocol, @unchecked Sendable {
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func pipe(_ command: String, timeout: TimeInterval) async -> ShellOutput {
|
||||
let trackingId = await commandTracker.track(command)
|
||||
defer {
|
||||
let tracker = self.commandTracker
|
||||
DispatchQueue.main.async { tracker.complete(trackingId) }
|
||||
}
|
||||
|
||||
func pipeRaw(_ command: String, timeout: TimeInterval) async -> ShellOutput {
|
||||
let process = getShellProcess(for: command)
|
||||
|
||||
let outputPipe = Pipe()
|
||||
@@ -311,17 +290,11 @@ class RealShell: ShellProtocol, @unchecked Sendable {
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func attach(
|
||||
func attachRaw(
|
||||
_ command: String,
|
||||
didReceiveOutput: @escaping (String, ShellStream) -> Void,
|
||||
withTimeout timeout: TimeInterval = 5.0
|
||||
) async throws -> (Process, ShellOutput) {
|
||||
let trackingId = await commandTracker.track(command)
|
||||
defer {
|
||||
let tracker = self.commandTracker
|
||||
DispatchQueue.main.async { tracker.complete(trackingId) }
|
||||
}
|
||||
|
||||
let process = getShellProcess(for: command)
|
||||
let outputPipe = Pipe(), errorPipe = Pipe()
|
||||
|
||||
|
||||
@@ -14,6 +14,19 @@ protocol ShellProtocol {
|
||||
*/
|
||||
var PATH: String { get }
|
||||
|
||||
/**
|
||||
Run a command synchronously without tracking. Use with caution!
|
||||
|
||||
Common usage:
|
||||
```
|
||||
let output = Shell.sync("php -v")
|
||||
```
|
||||
|
||||
@return The shell output. If the command times out, returns empty output.
|
||||
*/
|
||||
@discardableResult
|
||||
func syncRaw(_ command: String) -> ShellOutput
|
||||
|
||||
/**
|
||||
Run a command synchronously. Use with caution!
|
||||
|
||||
@@ -37,6 +50,9 @@ protocol ShellProtocol {
|
||||
|
||||
@return The shell output. If the command times out, returns empty output.
|
||||
*/
|
||||
@discardableResult
|
||||
func pipeRaw(_ command: String) async -> ShellOutput
|
||||
|
||||
@discardableResult
|
||||
func pipe(_ command: String) async -> ShellOutput
|
||||
|
||||
@@ -49,6 +65,9 @@ protocol ShellProtocol {
|
||||
|
||||
@return The shell output. If the command times out, returns empty output.
|
||||
*/
|
||||
@discardableResult
|
||||
func pipeRaw(_ command: String, timeout: TimeInterval) async -> ShellOutput
|
||||
|
||||
@discardableResult
|
||||
func pipe(_ command: String, timeout: TimeInterval) async -> ShellOutput
|
||||
|
||||
@@ -63,6 +82,13 @@ protocol ShellProtocol {
|
||||
|
||||
@return A tuple, containing the `Process` and `ShellOutput` objects.
|
||||
*/
|
||||
@discardableResult
|
||||
func attachRaw(
|
||||
_ command: String,
|
||||
didReceiveOutput: @escaping (String, ShellStream) -> Void,
|
||||
withTimeout timeout: TimeInterval
|
||||
) async throws -> (Process, ShellOutput)
|
||||
|
||||
@discardableResult
|
||||
func attach(
|
||||
_ command: String,
|
||||
@@ -76,6 +102,42 @@ protocol ShellProtocol {
|
||||
func reloadEnvPath()
|
||||
}
|
||||
|
||||
protocol TrackedShellProtocol: ShellProtocol, CommandTrackingProvider {}
|
||||
|
||||
extension TrackedShellProtocol {
|
||||
@discardableResult
|
||||
func sync(_ command: String) -> ShellOutput {
|
||||
trackedCommand(description: command) {
|
||||
syncRaw(command)
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func pipe(_ command: String) async -> ShellOutput {
|
||||
await trackedCommandAsync(description: command) {
|
||||
await pipeRaw(command)
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func pipe(_ command: String, timeout: TimeInterval) async -> ShellOutput {
|
||||
await trackedCommandAsync(description: command) {
|
||||
await pipeRaw(command, timeout: timeout)
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func attach(
|
||||
_ command: String,
|
||||
didReceiveOutput: @escaping (String, ShellStream) -> Void,
|
||||
withTimeout timeout: TimeInterval
|
||||
) async throws -> (Process, ShellOutput) {
|
||||
try await trackedCommandAsync(description: command) {
|
||||
try await attachRaw(command, didReceiveOutput: didReceiveOutput, withTimeout: timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ShellStream: Codable {
|
||||
case stdOut, stdErr, stdIn
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ public class TestableShell: ShellProtocol {
|
||||
var expectations: [String: BatchFakeShellOutput] = [:]
|
||||
|
||||
@discardableResult
|
||||
func sync(_ command: String) -> ShellOutput {
|
||||
func syncRaw(_ command: String) -> ShellOutput {
|
||||
// This assertion will only fire during test builds
|
||||
assert(expectations.keys.contains(command), "No response declared for command: \(command)")
|
||||
|
||||
@@ -32,18 +32,18 @@ public class TestableShell: ShellProtocol {
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func pipe(_ command: String) async -> ShellOutput {
|
||||
return await pipe(command, timeout: 60)
|
||||
func pipeRaw(_ command: String) async -> ShellOutput {
|
||||
await pipeRaw(command, timeout: 60)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func pipe(_ command: String, timeout: TimeInterval) async -> ShellOutput {
|
||||
let (_, output) = try! await self.attach(command, didReceiveOutput: { _, _ in }, withTimeout: timeout)
|
||||
func pipeRaw(_ command: String, timeout: TimeInterval) async -> ShellOutput {
|
||||
let (_, output) = try! await self.attachRaw(command, didReceiveOutput: { _, _ in }, withTimeout: timeout)
|
||||
return output
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func attach(
|
||||
func attachRaw(
|
||||
_ command: String,
|
||||
didReceiveOutput: @escaping (String, ShellStream) -> Void,
|
||||
withTimeout timeout: TimeInterval
|
||||
@@ -72,6 +72,30 @@ public class TestableShell: ShellProtocol {
|
||||
func reloadEnvPath() {
|
||||
// does nothing
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func sync(_ command: String) -> ShellOutput {
|
||||
syncRaw(command)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func pipe(_ command: String) async -> ShellOutput {
|
||||
await pipeRaw(command)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func pipe(_ command: String, timeout: TimeInterval) async -> ShellOutput {
|
||||
await pipeRaw(command, timeout: timeout)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func attach(
|
||||
_ command: String,
|
||||
didReceiveOutput: @escaping (String, ShellStream) -> Void,
|
||||
withTimeout timeout: TimeInterval
|
||||
) async throws -> (Process, ShellOutput) {
|
||||
try await attachRaw(command, didReceiveOutput: didReceiveOutput, withTimeout: timeout)
|
||||
}
|
||||
}
|
||||
|
||||
struct FakeShellOutput: Codable {
|
||||
|
||||
@@ -119,9 +119,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
||||
// Start with the regular busy icon
|
||||
MainMenu.shared.setStatusBar(image: NSImage.statusBarIcon)
|
||||
|
||||
// Show the active commands debug window
|
||||
ActiveCommandsWindowController.show()
|
||||
|
||||
Task { // Make sure the menu performs its initial checks
|
||||
await Startup.check(App.shared.container)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ typealias PhpConfigManagerWC = PhpConfigManagerWindowController
|
||||
typealias PhpDoctorWC = PhpDoctorWindowController
|
||||
typealias PhpVersionManagerWC = PhpVersionManagerWindowController
|
||||
typealias PhpExtensionManagerWC = PhpExtensionManagerWindowController
|
||||
typealias ActiveCommandsWC = ActiveCommandsWindowController
|
||||
typealias CommandHistoryWC = CommandHistoryWindowController
|
||||
|
||||
let WindowManager = WindowCoordinator.shared
|
||||
|
||||
|
||||
@@ -138,6 +138,12 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
}
|
||||
}
|
||||
|
||||
@objc func showCommandHistory() {
|
||||
Task { @MainActor in
|
||||
CommandHistoryWindowController.show()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func showIncompatiblePhpVersionsAlert() {
|
||||
Task { @MainActor in
|
||||
NVAlert().withInformation(
|
||||
|
||||
@@ -325,7 +325,8 @@ extension StatusMenu {
|
||||
// FIRST AID
|
||||
HeaderView.asMenuItem(text: "mi_first_aid".localized),
|
||||
NSMenuItem(title: "mi_view_onboarding".localized, action: #selector(MainMenu.showWelcomeTour)),
|
||||
NSMenuItem(title: "mi_fa_php_doctor".localized, action: #selector(MainMenu.openWarnings))
|
||||
NSMenuItem(title: "mi_fa_php_doctor".localized, action: #selector(MainMenu.openWarnings)),
|
||||
NSMenuItem(title: "mi_view_command_history".localized, action: #selector(MainMenu.showCommandHistory))
|
||||
]
|
||||
|
||||
if Valet.installed {
|
||||
|
||||
@@ -47,10 +47,10 @@ extension GenericPreferenceVC {
|
||||
name: windowName,
|
||||
frame: WindowManager.window(for: PhpExtensionManagerWC.self)?.frame
|
||||
)
|
||||
case "ActiveCommands":
|
||||
case "CommandHistory", "ActiveCommands":
|
||||
return WindowSnapshot(
|
||||
name: windowName,
|
||||
frame: WindowManager.window(for: ActiveCommandsWC.self)?.frame
|
||||
frame: WindowManager.window(for: CommandHistoryWC.self)?.frame
|
||||
)
|
||||
default:
|
||||
return nil
|
||||
@@ -79,9 +79,9 @@ extension GenericPreferenceVC {
|
||||
case "PhpExtensionManager":
|
||||
PhpExtensionManagerWindowController.show()
|
||||
applyFrame(snapshot.frame, for: PhpExtensionManagerWC.self)
|
||||
case "ActiveCommands":
|
||||
ActiveCommandsWindowController.show()
|
||||
applyFrame(snapshot.frame, for: ActiveCommandsWC.self)
|
||||
case "CommandHistory", "ActiveCommands":
|
||||
CommandHistoryWindowController.show()
|
||||
applyFrame(snapshot.frame, for: CommandHistoryWC.self)
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
//
|
||||
// ActiveCommandsView.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Copyright © 2025 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ActiveCommandsView: View {
|
||||
@ObservedObject var commandTracker: CommandTracker
|
||||
@State private var tick = false
|
||||
|
||||
let timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
|
||||
|
||||
init(commandTracker: CommandTracker? = nil) {
|
||||
self.commandTracker = commandTracker ?? App.shared.container.commandTracker
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollViewReader { proxy in
|
||||
List {
|
||||
if commandTracker.commands.isEmpty {
|
||||
HStack {
|
||||
Spacer()
|
||||
Text("No commands have been tracked yet.")
|
||||
.font(.system(size: 13))
|
||||
.foregroundColor(.secondary)
|
||||
.padding(30)
|
||||
Spacer()
|
||||
}
|
||||
} else {
|
||||
ForEach(commandTracker.commands) { command in
|
||||
HStack(spacing: 10) {
|
||||
if command.isCompleted {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(.green)
|
||||
.frame(width: 16)
|
||||
} else {
|
||||
ProgressView()
|
||||
.controlSize(.small)
|
||||
.frame(width: 16)
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(command.command)
|
||||
.font(.system(size: 12, design: .monospaced))
|
||||
.lineLimit(2)
|
||||
// tick forces re-evaluation every 200ms
|
||||
let _ = tick
|
||||
Text(command.durationText)
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
.id(command.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.onChange(of: commandTracker.commands.count) { _ in
|
||||
if let last = commandTracker.commands.last {
|
||||
proxy.scrollTo(last.id, anchor: .bottom)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(minWidth: 400, minHeight: 200)
|
||||
.onReceive(timer) { _ in
|
||||
if commandTracker.commands.contains(where: { !$0.isCompleted }) {
|
||||
tick.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
87
phpmon/Modules/Command History/UI/CommandHistoryView.swift
Normal file
87
phpmon/Modules/Command History/UI/CommandHistoryView.swift
Normal file
@@ -0,0 +1,87 @@
|
||||
//
|
||||
// CommandHistoryView.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Copyright © 2025 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CommandHistoryView: View {
|
||||
@ObservedObject var commandTracker: CommandTracker
|
||||
@State private var now = Date()
|
||||
|
||||
init(commandTracker: CommandTracker? = nil) {
|
||||
self.commandTracker = commandTracker ?? App.shared.container.commandTracker
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("This window displays the last executed (shell) commands. Keep in mind that only the last 200 commands are stored and displayed.")
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.top, 10)
|
||||
ScrollViewReader { proxy in
|
||||
List {
|
||||
if commandTracker.commands.isEmpty {
|
||||
HStack {
|
||||
Spacer()
|
||||
Text("No commands have been tracked yet.")
|
||||
.font(.system(size: 13))
|
||||
.foregroundColor(.secondary)
|
||||
.padding(30)
|
||||
Spacer()
|
||||
}
|
||||
} else {
|
||||
ForEach(commandTracker.commands) { command in
|
||||
HStack(alignment: .top, spacing: 10) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
||||
if command.isCompleted {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(.green)
|
||||
.frame(width: 16)
|
||||
} else {
|
||||
ProgressView()
|
||||
.controlSize(.small)
|
||||
.frame(width: 16)
|
||||
}
|
||||
Text(command.command)
|
||||
.font(.system(size: 12, design: .monospaced))
|
||||
.lineLimit(2)
|
||||
}
|
||||
|
||||
Text(command.durationText(at: now))
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
.id(command.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.onChange(of: commandTracker.commands.count) { _ in
|
||||
if let last = commandTracker.commands.last {
|
||||
proxy.scrollTo(last.id, anchor: .bottom)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(minWidth: 400, minHeight: 200)
|
||||
.onChange(of: commandTracker.isActive) { isActive in
|
||||
guard isActive else { return }
|
||||
now = Date()
|
||||
}
|
||||
.onReceive(
|
||||
Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
|
||||
) { _ in
|
||||
if commandTracker.isActive {
|
||||
now = Date()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
//
|
||||
// ActiveCommandsWindowController.swift
|
||||
// CommandHistoryWindowController.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Copyright © 2025 Nico Verbruggen. All rights reserved.
|
||||
@@ -8,12 +8,12 @@
|
||||
import Cocoa
|
||||
import SwiftUI
|
||||
|
||||
class ActiveCommandsWindowController: PMWindowController {
|
||||
class CommandHistoryWindowController: PMWindowController {
|
||||
|
||||
// MARK: - Window Identifier
|
||||
|
||||
override var windowName: String {
|
||||
return "ActiveCommands"
|
||||
return "CommandHistory"
|
||||
}
|
||||
|
||||
public static func create(delegate: NSWindowDelegate?) {
|
||||
@@ -21,12 +21,12 @@ class ActiveCommandsWindowController: PMWindowController {
|
||||
|
||||
let panel = NSPanel()
|
||||
panel.styleMask = [.titled, .closable, .miniaturizable, .resizable, .utilityWindow]
|
||||
panel.title = "Active Commands"
|
||||
panel.title = "Command History"
|
||||
panel.titlebarAppearsTransparent = true
|
||||
panel.isFloatingPanel = true
|
||||
panel.hidesOnDeactivate = false
|
||||
panel.delegate = delegate ?? windowController
|
||||
panel.contentView = NSHostingView(rootView: ActiveCommandsView())
|
||||
panel.contentView = NSHostingView(rootView: CommandHistoryView())
|
||||
panel.setContentSize(NSSize(width: 500, height: 300))
|
||||
|
||||
windowController.window = panel
|
||||
@@ -35,12 +35,12 @@ class ActiveCommandsWindowController: PMWindowController {
|
||||
}
|
||||
|
||||
public static func show(delegate: NSWindowDelegate? = nil) {
|
||||
if !WindowManager.hasController(for: ActiveCommandsWC.self) {
|
||||
if !WindowManager.hasController(for: CommandHistoryWC.self) {
|
||||
Self.create(delegate: delegate)
|
||||
}
|
||||
|
||||
WindowManager.show(ActiveCommandsWC.self)
|
||||
WindowManager.withWindow(for: ActiveCommandsWC.self) { window in
|
||||
WindowManager.show(CommandHistoryWC.self)
|
||||
WindowManager.withWindow(for: CommandHistoryWC.self) { window in
|
||||
window.setCenterPosition(offsetY: 70)
|
||||
}
|
||||
}
|
||||
@@ -84,6 +84,7 @@
|
||||
"mi_set_up_presets" = "Learn more about presets...";
|
||||
|
||||
"mi_view_onboarding" = "Open Welcome Tour...";
|
||||
"mi_view_command_history" = "Open Command History...";
|
||||
|
||||
"mi_xdebug_available_modes" = "Available Modes";
|
||||
"mi_xdebug_actions" = "Actions";
|
||||
|
||||
Reference in New Issue
Block a user