1
0
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:
2026-02-24 14:43:55 +01:00
parent b445c2aa4f
commit 6f5501573a
18 changed files with 431 additions and 267 deletions

View File

@@ -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 */,

View File

@@ -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
)
}
}
}

View 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()
}
}

View File

@@ -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 = ""

View File

@@ -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]!

View File

@@ -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)")
}
}
}

View File

@@ -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()

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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(

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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()
}
}
}
}

View 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()
}
}
}
}
}

View File

@@ -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)
}
}

View File

@@ -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";