diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index b8c3776a..79097a06 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -14,6 +14,10 @@ 0317C17E2ED87CAB005479D2 /* NVAppUpdater in Frameworks */ = {isa = PBXBuildFile; productRef = 0317C17D2ED87CAB005479D2 /* NVAppUpdater */; }; 0317C1812ED87CE1005479D2 /* NVAppUpdater in Frameworks */ = {isa = PBXBuildFile; productRef = 0317C1802ED87CE1005479D2 /* NVAppUpdater */; }; 0317C1832ED87CEA005479D2 /* NVAppUpdater in Frameworks */ = {isa = PBXBuildFile; productRef = 0317C1822ED87CEA005479D2 /* NVAppUpdater */; }; + 031A80DC2F4CF1690016F7DD /* CommandTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031A80DB2F4CF1690016F7DD /* CommandTracker.swift */; }; + 031A80DD2F4CF1690016F7DD /* CommandTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031A80DB2F4CF1690016F7DD /* CommandTracker.swift */; }; + 031A80DE2F4CF1690016F7DD /* CommandTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031A80DB2F4CF1690016F7DD /* CommandTracker.swift */; }; + 031A80DF2F4CF1690016F7DD /* CommandTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031A80DB2F4CF1690016F7DD /* CommandTracker.swift */; }; 031D747C2F46225600D4FF48 /* SimpleButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031D747B2F46225600D4FF48 /* SimpleButton.swift */; }; 031D747D2F46225600D4FF48 /* SimpleButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031D747B2F46225600D4FF48 /* SimpleButton.swift */; }; 031D747E2F46225600D4FF48 /* SimpleButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031D747B2F46225600D4FF48 /* SimpleButton.swift */; }; @@ -317,6 +321,7 @@ 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 */; }; @@ -632,6 +637,7 @@ 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 */; }; @@ -652,6 +658,7 @@ 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 */; }; @@ -713,6 +720,7 @@ 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 */; }; @@ -733,6 +741,7 @@ 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 */; }; @@ -766,7 +775,9 @@ 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 */; }; @@ -777,6 +788,7 @@ 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 */; }; @@ -1053,6 +1065,7 @@ /* Begin PBXFileReference section */ 0309E6662B0D4B2F002AC007 /* BrewExtensionsObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewExtensionsObservable.swift; sourceTree = ""; }; + 031A80DB2F4CF1690016F7DD /* CommandTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandTracker.swift; sourceTree = ""; }; 031D747B2F46225600D4FF48 /* SimpleButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleButton.swift; sourceTree = ""; }; 031D74802F46225C00D4FF48 /* SelectDomainTypeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectDomainTypeView.swift; sourceTree = ""; }; 031D74852F46306F00D4FF48 /* AddSiteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSiteView.swift; sourceTree = ""; }; @@ -1185,6 +1198,8 @@ C42106652AFA9FF400DF3732 /* PhpVersionManagerView+Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PhpVersionManagerView+Actions.swift"; sourceTree = ""; }; C422DDA928A2C49900CEAC97 /* PhpDoctorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpDoctorView.swift; sourceTree = ""; }; C422DDAC28A2DAC600CEAC97 /* PhpDoctorWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpDoctorWindowController.swift; sourceTree = ""; }; + E51118AFDD02AA1B7D8C9278 /* ActiveCommandsWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveCommandsWindowController.swift; sourceTree = ""; }; + 31503E15DADA980998F0F5A2 /* ActiveCommandsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveCommandsView.swift; sourceTree = ""; }; C4232EE42612526500158FC6 /* Credits.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = Credits.html; sourceTree = ""; }; C42337A2281F19F000459A48 /* Xdebug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Xdebug.swift; sourceTree = ""; }; C42759662627662800093CAE /* NSMenuExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSMenuExtension.swift; sourceTree = ""; }; @@ -1710,6 +1725,7 @@ C40C7F2F27722E8D00DDDCDC /* Logger.swift */, C417DC73277614690015E6EE /* Helpers.swift */, C4CB6E64292C362C002E9027 /* Homebrew.swift */, + 031A80DB2F4CF1690016F7DD /* CommandTracker.swift */, ); path = Core; sourceTree = ""; @@ -1959,6 +1975,23 @@ path = Data; sourceTree = ""; }; + F0F42C1C96504D46C75AD6F8 /* UI */ = { + isa = PBXGroup; + children = ( + E51118AFDD02AA1B7D8C9278 /* ActiveCommandsWindowController.swift */, + 31503E15DADA980998F0F5A2 /* ActiveCommandsView.swift */, + ); + path = UI; + sourceTree = ""; + }; + E1E4FBD76E71C469A65AC4F8 /* Active Commands */ = { + isa = PBXGroup; + children = ( + F0F42C1C96504D46C75AD6F8 /* UI */, + ); + path = "Active Commands"; + sourceTree = ""; + }; C44DFA832A6706A200B98ED5 /* Modules */ = { isa = PBXGroup; children = ( @@ -1966,6 +1999,7 @@ C464ADAA275A7A25003FCD53 /* Domain List */, C44DFA7A2A6703FD00B98ED5 /* PHP Config Editor */, C4297F7828970D4E004C4630 /* PHP Doctor */, + E1E4FBD76E71C469A65AC4F8 /* Active Commands */, C43931C329C4BD510069165B /* PHP Version Manager */, C4292D512B023F37004F0D2A /* PHP Extension Manager */, ); @@ -2990,6 +3024,8 @@ 03D846352EB64E39006EFE3C /* CrashReporter.swift in Sources */, C42759672627662800093CAE /* NSMenuExtension.swift in Sources */, C422DDAA28A2C49900CEAC97 /* PhpDoctorView.swift in Sources */, + 719E6363E4F41C8A599FCC99 /* ActiveCommandsView.swift in Sources */, + 031A80DE2F4CF1690016F7DD /* CommandTracker.swift in Sources */, C469E6FE294CF7B200A82AB2 /* FakeValetProxy.swift in Sources */, C464ADAF275A7A69003FCD53 /* DomainListVC.swift in Sources */, C44CCD4927AFF3B700CE40E5 /* MainMenu+Async.swift in Sources */, @@ -3027,6 +3063,7 @@ 031D74842F46225C00D4FF48 /* SelectDomainTypeView.swift in Sources */, C4D5CFCA27E0F9CD00035329 /* NginxConfigurationFile.swift in Sources */, C485707028BF452300539B36 /* PhpDoctorWindowController.swift in Sources */, + 5FD883E3DBC963F53E12A17F /* ActiveCommandsWindowController.swift in Sources */, C4CE3BBA27B31F670086CA49 /* ComposerWindow.swift in Sources */, C40D725A2A018ACC0054A067 /* BusyStatus.swift in Sources */, 031F24802EA1071A00CFB8D9 /* Container+Fake.swift in Sources */, @@ -3166,6 +3203,7 @@ C45B9150295608E300F4EC78 /* ValetServicesManager.swift in Sources */, C471E86528F9BB650021E251 /* WarningManager.swift in Sources */, C471E86628F9BB650021E251 /* PhpDoctorWindowController.swift in Sources */, + DE164E569298309E4224C5D6 /* ActiveCommandsWindowController.swift in Sources */, C471E86728F9BB650021E251 /* OnboardingWindowController.swift in Sources */, C471E86828F9BB650021E251 /* PreferencesWindowController.swift in Sources */, C471E86928F9BB650021E251 /* PreferencesWindowController+Hotkey.swift in Sources */, @@ -3198,6 +3236,7 @@ C471E87E28F9BB650021E251 /* PresetHelper.swift in Sources */, C471E87F28F9BB650021E251 /* WarningView.swift in Sources */, C471E88028F9BB650021E251 /* PhpDoctorView.swift in Sources */, + 25250C95E34D20ED4C91C320 /* ActiveCommandsView.swift in Sources */, C471E88128F9BB650021E251 /* NoWarningsView.swift in Sources */, C471E88228F9BB650021E251 /* OnboardingView.swift in Sources */, C471E88328F9BB650021E251 /* VersionPopoverView.swift in Sources */, @@ -3220,6 +3259,7 @@ C471E7E928F9BAC20021E251 /* Paths.swift in Sources */, C43B8FD72BA9C689000C02BE /* UnavailableContentView.swift in Sources */, C45B91552956123A00F4EC78 /* FakeServicesManager.swift in Sources */, + 031A80DF2F4CF1690016F7DD /* CommandTracker.swift in Sources */, C471E7FE28F9BACE0021E251 /* HomebrewDecodable.swift in Sources */, C4415E8F2B0287E90035F520 /* BrewFormulaeObservable.swift in Sources */, C471E7D828F9BA8F0021E251 /* FileSystemProtocol.swift in Sources */, @@ -3394,11 +3434,13 @@ C45B91562956123A00F4EC78 /* FakeServicesManager.swift in Sources */, C471E8C628F9BB8F0021E251 /* PMTableView.swift in Sources */, C471E8C728F9BB8F0021E251 /* Warning.swift in Sources */, + 031A80DC2F4CF1690016F7DD /* CommandTracker.swift in Sources */, 033E9DFC2F44D93000685F62 /* Startup+Alert.swift in Sources */, 031D74822F46225C00D4FF48 /* SelectDomainTypeView.swift in Sources */, C471E8C828F9BB8F0021E251 /* WarningManager.swift in Sources */, C46DC7A72C7B5BCA00F19D17 /* Favorites.swift in Sources */, C471E8C928F9BB8F0021E251 /* PhpDoctorWindowController.swift in Sources */, + BB3AF45BED2C82AE27E86107 /* ActiveCommandsWindowController.swift in Sources */, C41ADCEB2970CCC700120423 /* FSNotifier.swift in Sources */, C471E8CA28F9BB8F0021E251 /* OnboardingWindowController.swift in Sources */, C456A0C92AA614BD0080144F /* PhpPreference.swift in Sources */, @@ -3437,6 +3479,7 @@ 031D747D2F46225600D4FF48 /* SimpleButton.swift in Sources */, C471E8E228F9BB8F0021E251 /* WarningView.swift in Sources */, C471E8E328F9BB8F0021E251 /* PhpDoctorView.swift in Sources */, + 0A1A6208D3DD2495FBD8569B /* ActiveCommandsView.swift in Sources */, C471E8E428F9BB8F0021E251 /* NoWarningsView.swift in Sources */, C471E8E528F9BB8F0021E251 /* OnboardingView.swift in Sources */, C4B79EBF29CA38DB00A483EE /* BrewCommand.swift in Sources */, @@ -3572,6 +3615,7 @@ C415D3B82770F294005EF286 /* Actions.swift in Sources */, 54B48B60275F66AE006D90C5 /* Application.swift in Sources */, 039C291A2E8AA314007F5FAB /* TestableWebApi.swift in Sources */, + 031A80DD2F4CF1690016F7DD /* CommandTracker.swift in Sources */, 038A2B802EDDB24C00173ACF /* App+UUID.swift in Sources */, 03C099472EA15C8E00B76D43 /* Container+Real.swift in Sources */, C4D3661B291173EA006BD146 /* DictionaryExtension.swift in Sources */, @@ -3597,6 +3641,7 @@ C41ADCE92970CCC700120423 /* FSNotifier.swift in Sources */, C40C7F2927721FF600DDDCDC /* Valet+Alerts.swift in Sources */, C485707A28BF457800539B36 /* PhpDoctorView.swift in Sources */, + 4181B8F1C0ED930B2C5E5532 /* ActiveCommandsView.swift in Sources */, C4C0E8E827F88B41002D32A9 /* DomainScanner.swift in Sources */, C449B4F027EE7FB800C47E8A /* DomainListTLSCell.swift in Sources */, 037F44162EDB0AAA002EBF75 /* FSNotifierTest.swift in Sources */, @@ -3781,6 +3826,7 @@ C449B4F327EE7FC600C47E8A /* DomainListTypeCell.swift in Sources */, C48D6C71279CD2AC00F26D7E /* VersionNumber.swift in Sources */, C485706F28BF452300539B36 /* PhpDoctorWindowController.swift in Sources */, + 54D6418DBC6AF23FED489018 /* ActiveCommandsWindowController.swift in Sources */, C46FA9892822EFDC00D78807 /* PhpConfigurationFile.swift in Sources */, C43931CB29C4C03F0069165B /* Brew.swift in Sources */, 0392CDEC2EB25371009176DA /* SecurePopoverView.swift in Sources */, diff --git a/phpmon/Common/Command/RealCommand.swift b/phpmon/Common/Command/RealCommand.swift index 7c21cf60..3b75eea5 100644 --- a/phpmon/Common/Command/RealCommand.swift +++ b/phpmon/Common/Command/RealCommand.swift @@ -8,12 +8,28 @@ import Cocoa public class RealCommand: CommandProtocol { + private let commandTracker: CommandTracker + + init(commandTracker: CommandTracker) { + self.commandTracker = commandTracker + } + public func execute( 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 = "" diff --git a/phpmon/Common/Core/CommandTracker.swift b/phpmon/Common/Core/CommandTracker.swift new file mode 100644 index 00000000..842aca87 --- /dev/null +++ b/phpmon/Common/Core/CommandTracker.swift @@ -0,0 +1,76 @@ +// +// 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)") + } + } +} diff --git a/phpmon/Common/Shell/RealShell.swift b/phpmon/Common/Shell/RealShell.swift index f62ffc02..ac8255fc 100644 --- a/phpmon/Common/Shell/RealShell.swift +++ b/phpmon/Common/Shell/RealShell.swift @@ -10,12 +10,14 @@ import Foundation @preconcurrency import Dispatch class RealShell: ShellProtocol, @unchecked Sendable { - init(binPath: String) { + init(binPath: String, commandTracker: CommandTracker) { self.binPath = binPath + self.commandTracker = commandTracker self._PATH = RealShell.getPath() self._exports = [:] } + private let commandTracker: CommandTracker private(set) var binPath: String /** @@ -156,6 +158,15 @@ 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) } + } + } + let process = getShellProcess(for: command) let outputPipe = Pipe() @@ -187,6 +198,12 @@ 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) } + } + let process = getShellProcess(for: command) let outputPipe = Pipe() @@ -223,6 +240,12 @@ 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) } + } + let process = getShellProcess(for: command) let outputPipe = Pipe() @@ -293,6 +316,12 @@ class RealShell: ShellProtocol, @unchecked Sendable { 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() diff --git a/phpmon/Container/Container.swift b/phpmon/Container/Container.swift index 4a722a99..03c39ef4 100644 --- a/phpmon/Container/Container.swift +++ b/phpmon/Container/Container.swift @@ -17,6 +17,7 @@ class Container: @unchecked Sendable { private(set) var shell: ShellProtocol! private(set) var command: CommandProtocol! private(set) var webApi: WebApiProtocol! + private(set) var commandTracker: CommandTracker! // Secondary (uses primary instances above) private(set) var preferences: Preferences! @@ -67,8 +68,9 @@ class Container: @unchecked Sendable { // any of the other classes can be initialized! self.filesystem = RealFileSystem(container: self) self.paths = Paths(container: self) - self.shell = RealShell(binPath: paths.binPath) - self.command = RealCommand() + self.commandTracker = CommandTracker() + self.shell = RealShell(binPath: paths.binPath, commandTracker: commandTracker) + self.command = RealCommand(commandTracker: commandTracker) self.webApi = RealWebApi(container: self) if coreOnly { @@ -97,6 +99,7 @@ class Container: @unchecked Sendable { webApiGetResponses: [URL: FakeWebApiResponse] = [:], webApiPostResponses: [URL: FakeWebApiResponse] = [:] ) { + self.commandTracker = CommandTracker() self.shell = TestableShell(expectations: shellExpectations) self.filesystem = TestableFileSystem(files: fileSystemFiles) self.command = TestableCommand(commands: commands) diff --git a/phpmon/Domain/App/AppDelegate.swift b/phpmon/Domain/App/AppDelegate.swift index 20821ea9..49bae2e9 100644 --- a/phpmon/Domain/App/AppDelegate.swift +++ b/phpmon/Domain/App/AppDelegate.swift @@ -119,6 +119,9 @@ 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) } diff --git a/phpmon/Domain/App/WindowManager.swift b/phpmon/Domain/App/WindowManager.swift index 3875a251..3f56c0db 100644 --- a/phpmon/Domain/App/WindowManager.swift +++ b/phpmon/Domain/App/WindowManager.swift @@ -15,6 +15,7 @@ typealias PhpConfigManagerWC = PhpConfigManagerWindowController typealias PhpDoctorWC = PhpDoctorWindowController typealias PhpVersionManagerWC = PhpVersionManagerWindowController typealias PhpExtensionManagerWC = PhpExtensionManagerWindowController +typealias ActiveCommandsWC = ActiveCommandsWindowController let WindowManager = WindowCoordinator.shared diff --git a/phpmon/Domain/Preferences/PreferencesVC+WindowRestore.swift b/phpmon/Domain/Preferences/PreferencesVC+WindowRestore.swift index a5332ee9..db600840 100644 --- a/phpmon/Domain/Preferences/PreferencesVC+WindowRestore.swift +++ b/phpmon/Domain/Preferences/PreferencesVC+WindowRestore.swift @@ -47,6 +47,11 @@ extension GenericPreferenceVC { name: windowName, frame: WindowManager.window(for: PhpExtensionManagerWC.self)?.frame ) + case "ActiveCommands": + return WindowSnapshot( + name: windowName, + frame: WindowManager.window(for: ActiveCommandsWC.self)?.frame + ) default: return nil } @@ -74,6 +79,9 @@ extension GenericPreferenceVC { case "PhpExtensionManager": PhpExtensionManagerWindowController.show() applyFrame(snapshot.frame, for: PhpExtensionManagerWC.self) + case "ActiveCommands": + ActiveCommandsWindowController.show() + applyFrame(snapshot.frame, for: ActiveCommandsWC.self) default: continue } diff --git a/phpmon/Modules/Active Commands/UI/ActiveCommandsView.swift b/phpmon/Modules/Active Commands/UI/ActiveCommandsView.swift new file mode 100644 index 00000000..a9797987 --- /dev/null +++ b/phpmon/Modules/Active Commands/UI/ActiveCommandsView.swift @@ -0,0 +1,75 @@ +// +// 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() + } + } + } +} diff --git a/phpmon/Modules/Active Commands/UI/ActiveCommandsWindowController.swift b/phpmon/Modules/Active Commands/UI/ActiveCommandsWindowController.swift new file mode 100644 index 00000000..393c067a --- /dev/null +++ b/phpmon/Modules/Active Commands/UI/ActiveCommandsWindowController.swift @@ -0,0 +1,47 @@ +// +// ActiveCommandsWindowController.swift +// PHP Monitor +// +// Copyright © 2025 Nico Verbruggen. All rights reserved. +// + +import Cocoa +import SwiftUI + +class ActiveCommandsWindowController: PMWindowController { + + // MARK: - Window Identifier + + override var windowName: String { + return "ActiveCommands" + } + + public static func create(delegate: NSWindowDelegate?) { + let windowController = Self() + + let panel = NSPanel() + panel.styleMask = [.titled, .closable, .miniaturizable, .resizable, .utilityWindow] + panel.title = "Active Commands" + panel.titlebarAppearsTransparent = true + panel.isFloatingPanel = true + panel.hidesOnDeactivate = false + panel.delegate = delegate ?? windowController + panel.contentView = NSHostingView(rootView: ActiveCommandsView()) + panel.setContentSize(NSSize(width: 500, height: 300)) + + windowController.window = panel + + WindowManager.setController(windowController) + } + + public static func show(delegate: NSWindowDelegate? = nil) { + if !WindowManager.hasController(for: ActiveCommandsWC.self) { + Self.create(delegate: delegate) + } + + WindowManager.show(ActiveCommandsWC.self) + WindowManager.withWindow(for: ActiveCommandsWC.self) { window in + window.setCenterPosition(offsetY: 70) + } + } +}