1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2026-04-02 17:40:08 +02:00

♻️ Refactor file watchers

This commit is contained in:
2025-11-29 22:33:39 +01:00
parent ac28e4425b
commit e968263568
12 changed files with 361 additions and 318 deletions

View File

@@ -79,6 +79,14 @@
037F44192EDB27BA002EBF75 /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F44172EDB27B7002EBF75 /* Debouncer.swift */; }; 037F44192EDB27BA002EBF75 /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F44172EDB27B7002EBF75 /* Debouncer.swift */; };
037F441A2EDB27BA002EBF75 /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F44172EDB27B7002EBF75 /* Debouncer.swift */; }; 037F441A2EDB27BA002EBF75 /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F44172EDB27B7002EBF75 /* Debouncer.swift */; };
037F441B2EDB27BA002EBF75 /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F44172EDB27B7002EBF75 /* Debouncer.swift */; }; 037F441B2EDB27BA002EBF75 /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F44172EDB27B7002EBF75 /* Debouncer.swift */; };
037F441D2EDB9195002EBF75 /* ConfigWatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F441C2EDB9195002EBF75 /* ConfigWatchManager.swift */; };
037F441E2EDB9195002EBF75 /* ConfigWatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F441C2EDB9195002EBF75 /* ConfigWatchManager.swift */; };
037F441F2EDB9195002EBF75 /* ConfigWatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F441C2EDB9195002EBF75 /* ConfigWatchManager.swift */; };
037F44202EDB9195002EBF75 /* ConfigWatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F441C2EDB9195002EBF75 /* ConfigWatchManager.swift */; };
037F44222EDB92EC002EBF75 /* HomebrewWatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F44212EDB92EC002EBF75 /* HomebrewWatchManager.swift */; };
037F44232EDB92EC002EBF75 /* HomebrewWatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F44212EDB92EC002EBF75 /* HomebrewWatchManager.swift */; };
037F44242EDB92EC002EBF75 /* HomebrewWatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F44212EDB92EC002EBF75 /* HomebrewWatchManager.swift */; };
037F44252EDB92EC002EBF75 /* HomebrewWatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F44212EDB92EC002EBF75 /* HomebrewWatchManager.swift */; };
0386B0B42ED36C3D00CA6795 /* Locked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0386B0B32ED36C3D00CA6795 /* Locked.swift */; }; 0386B0B42ED36C3D00CA6795 /* Locked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0386B0B32ED36C3D00CA6795 /* Locked.swift */; };
0386B0B52ED36C3D00CA6795 /* Locked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0386B0B32ED36C3D00CA6795 /* Locked.swift */; }; 0386B0B52ED36C3D00CA6795 /* Locked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0386B0B32ED36C3D00CA6795 /* Locked.swift */; };
0386B0B62ED36C3D00CA6795 /* Locked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0386B0B32ED36C3D00CA6795 /* Locked.swift */; }; 0386B0B62ED36C3D00CA6795 /* Locked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0386B0B32ED36C3D00CA6795 /* Locked.swift */; };
@@ -318,10 +326,6 @@
C4415E8E2B0287E90035F520 /* BrewFormulaeObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4415E8C2B0287E90035F520 /* BrewFormulaeObservable.swift */; }; C4415E8E2B0287E90035F520 /* BrewFormulaeObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4415E8C2B0287E90035F520 /* BrewFormulaeObservable.swift */; };
C4415E8F2B0287E90035F520 /* BrewFormulaeObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4415E8C2B0287E90035F520 /* BrewFormulaeObservable.swift */; }; C4415E8F2B0287E90035F520 /* BrewFormulaeObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4415E8C2B0287E90035F520 /* BrewFormulaeObservable.swift */; };
C4415E902B0287E90035F520 /* BrewFormulaeObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4415E8C2B0287E90035F520 /* BrewFormulaeObservable.swift */; }; C4415E902B0287E90035F520 /* BrewFormulaeObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4415E8C2B0287E90035F520 /* BrewFormulaeObservable.swift */; };
C441CC562AE8249400DDFACD /* ConfigFSNotifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C441CC552AE8249400DDFACD /* ConfigFSNotifier.swift */; };
C441CC572AE8249400DDFACD /* ConfigFSNotifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C441CC552AE8249400DDFACD /* ConfigFSNotifier.swift */; };
C441CC582AE8249400DDFACD /* ConfigFSNotifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C441CC552AE8249400DDFACD /* ConfigFSNotifier.swift */; };
C441CC592AE8249400DDFACD /* ConfigFSNotifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C441CC552AE8249400DDFACD /* ConfigFSNotifier.swift */; };
C44264BE2850B86C007400F1 /* SwiftUIHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44264BD2850B86C007400F1 /* SwiftUIHelper.swift */; }; C44264BE2850B86C007400F1 /* SwiftUIHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44264BD2850B86C007400F1 /* SwiftUIHelper.swift */; };
C44264C02850BD2A007400F1 /* VersionPopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44264BF2850BD2A007400F1 /* VersionPopoverView.swift */; }; C44264C02850BD2A007400F1 /* VersionPopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44264BF2850BD2A007400F1 /* VersionPopoverView.swift */; };
C4463FCC29804BCB007B93D5 /* RCFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4463FCB29804BCB007B93D5 /* RCFile.swift */; }; C4463FCC29804BCB007B93D5 /* RCFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4463FCB29804BCB007B93D5 /* RCFile.swift */; };
@@ -604,8 +608,6 @@
C471E87728F9BB650021E251 /* Keys.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CDA892288F1A71007CE25F /* Keys.swift */; }; C471E87728F9BB650021E251 /* Keys.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CDA892288F1A71007CE25F /* Keys.swift */; };
C471E87828F9BB650021E251 /* TerminalProgressWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44C198C276E3A1C0072762D /* TerminalProgressWindowController.swift */; }; C471E87828F9BB650021E251 /* TerminalProgressWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44C198C276E3A1C0072762D /* TerminalProgressWindowController.swift */; };
C471E87928F9BB650021E251 /* ProgressVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44A874728905BB000498BC4 /* ProgressVC.swift */; }; C471E87928F9BB650021E251 /* ProgressVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44A874728905BB000498BC4 /* ProgressVC.swift */; };
C471E87B28F9BB650021E251 /* App+ConfigWatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E817276F54D8003AC782 /* App+ConfigWatch.swift */; };
C471E87C28F9BB650021E251 /* ConfigWatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E81A276F54E5003AC782 /* ConfigWatchManager.swift */; };
C471E87D28F9BB650021E251 /* Preset.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C5C9B2846A40600E28255 /* Preset.swift */; }; C471E87D28F9BB650021E251 /* Preset.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C5C9B2846A40600E28255 /* Preset.swift */; };
C471E87E28F9BB650021E251 /* PresetHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C463E37F284930EE00422731 /* PresetHelper.swift */; }; C471E87E28F9BB650021E251 /* PresetHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C463E37F284930EE00422731 /* PresetHelper.swift */; };
C471E87F28F9BB650021E251 /* WarningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4297F7928970D59004C4630 /* WarningView.swift */; }; C471E87F28F9BB650021E251 /* WarningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4297F7928970D59004C4630 /* WarningView.swift */; };
@@ -690,8 +692,6 @@
C471E8DA28F9BB8F0021E251 /* Keys.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CDA892288F1A71007CE25F /* Keys.swift */; }; C471E8DA28F9BB8F0021E251 /* Keys.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CDA892288F1A71007CE25F /* Keys.swift */; };
C471E8DB28F9BB8F0021E251 /* TerminalProgressWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44C198C276E3A1C0072762D /* TerminalProgressWindowController.swift */; }; C471E8DB28F9BB8F0021E251 /* TerminalProgressWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44C198C276E3A1C0072762D /* TerminalProgressWindowController.swift */; };
C471E8DC28F9BB8F0021E251 /* ProgressVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44A874728905BB000498BC4 /* ProgressVC.swift */; }; C471E8DC28F9BB8F0021E251 /* ProgressVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44A874728905BB000498BC4 /* ProgressVC.swift */; };
C471E8DE28F9BB8F0021E251 /* App+ConfigWatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E817276F54D8003AC782 /* App+ConfigWatch.swift */; };
C471E8DF28F9BB8F0021E251 /* ConfigWatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E81A276F54E5003AC782 /* ConfigWatchManager.swift */; };
C471E8E028F9BB8F0021E251 /* Preset.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C5C9B2846A40600E28255 /* Preset.swift */; }; C471E8E028F9BB8F0021E251 /* Preset.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C5C9B2846A40600E28255 /* Preset.swift */; };
C471E8E128F9BB8F0021E251 /* PresetHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C463E37F284930EE00422731 /* PresetHelper.swift */; }; C471E8E128F9BB8F0021E251 /* PresetHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C463E37F284930EE00422731 /* PresetHelper.swift */; };
C471E8E228F9BB8F0021E251 /* WarningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4297F7928970D59004C4630 /* WarningView.swift */; }; C471E8E228F9BB8F0021E251 /* WarningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4297F7928970D59004C4630 /* WarningView.swift */; };
@@ -755,10 +755,6 @@
C48DDD0E29C75C9E00D032D9 /* BlockingOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48DDD0C29C75C9E00D032D9 /* BlockingOverlayView.swift */; }; C48DDD0E29C75C9E00D032D9 /* BlockingOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48DDD0C29C75C9E00D032D9 /* BlockingOverlayView.swift */; };
C48DDD0F29C75C9E00D032D9 /* BlockingOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48DDD0C29C75C9E00D032D9 /* BlockingOverlayView.swift */; }; C48DDD0F29C75C9E00D032D9 /* BlockingOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48DDD0C29C75C9E00D032D9 /* BlockingOverlayView.swift */; };
C48DDD1029C75C9E00D032D9 /* BlockingOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48DDD0C29C75C9E00D032D9 /* BlockingOverlayView.swift */; }; C48DDD1029C75C9E00D032D9 /* BlockingOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48DDD0C29C75C9E00D032D9 /* BlockingOverlayView.swift */; };
C490E3B629BCA367006D2DE6 /* App+BrewWatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EAA5629B1689200AB28FC /* App+BrewWatch.swift */; };
C490E3B829BCA367006D2DE6 /* App+BrewWatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EAA5629B1689200AB28FC /* App+BrewWatch.swift */; };
C490E3B929BCA368006D2DE6 /* App+BrewWatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EAA5629B1689200AB28FC /* App+BrewWatch.swift */; };
C490E3BA29BCA368006D2DE6 /* App+BrewWatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EAA5629B1689200AB28FC /* App+BrewWatch.swift */; };
C490E3BB29BCA375006D2DE6 /* Measurements.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EAA5129B12A5A00AB28FC /* Measurements.swift */; }; C490E3BB29BCA375006D2DE6 /* Measurements.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EAA5129B12A5A00AB28FC /* Measurements.swift */; };
C490E3BC29BCA375006D2DE6 /* Measurements.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EAA5129B12A5A00AB28FC /* Measurements.swift */; }; C490E3BC29BCA375006D2DE6 /* Measurements.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EAA5129B12A5A00AB28FC /* Measurements.swift */; };
C490E3BD29BCA375006D2DE6 /* Measurements.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EAA5129B12A5A00AB28FC /* Measurements.swift */; }; C490E3BD29BCA375006D2DE6 /* Measurements.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EAA5129B12A5A00AB28FC /* Measurements.swift */; };
@@ -845,10 +841,6 @@
C4C3ED4327834C5200AB15D8 /* CustomPrefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3ED4227834C5200AB15D8 /* CustomPrefs.swift */; }; C4C3ED4327834C5200AB15D8 /* CustomPrefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3ED4227834C5200AB15D8 /* CustomPrefs.swift */; };
C4C8900328F0E28800CE5E97 /* FileSystemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8900228F0E28800CE5E97 /* FileSystemProtocol.swift */; }; C4C8900328F0E28800CE5E97 /* FileSystemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8900228F0E28800CE5E97 /* FileSystemProtocol.swift */; };
C4C8900528F0E3D100CE5E97 /* RealFileSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8900428F0E3D100CE5E97 /* RealFileSystem.swift */; }; C4C8900528F0E3D100CE5E97 /* RealFileSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8900428F0E3D100CE5E97 /* RealFileSystem.swift */; };
C4C8E818276F54D8003AC782 /* App+ConfigWatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E817276F54D8003AC782 /* App+ConfigWatch.swift */; };
C4C8E819276F54D8003AC782 /* App+ConfigWatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E817276F54D8003AC782 /* App+ConfigWatch.swift */; };
C4C8E81B276F54E5003AC782 /* ConfigWatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E81A276F54E5003AC782 /* ConfigWatchManager.swift */; };
C4C8E81C276F54E5003AC782 /* ConfigWatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E81A276F54E5003AC782 /* ConfigWatchManager.swift */; };
C4CB250529B28BB800CA4492 /* MainMenuTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CB250429B28BB800CA4492 /* MainMenuTest.swift */; }; C4CB250529B28BB800CA4492 /* MainMenuTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CB250429B28BB800CA4492 /* MainMenuTest.swift */; };
C4CB6E65292C362C002E9027 /* Homebrew.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CB6E64292C362C002E9027 /* Homebrew.swift */; }; C4CB6E65292C362C002E9027 /* Homebrew.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CB6E64292C362C002E9027 /* Homebrew.swift */; };
C4CB6E66292C362C002E9027 /* Homebrew.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CB6E64292C362C002E9027 /* Homebrew.swift */; }; C4CB6E66292C362C002E9027 /* Homebrew.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CB6E64292C362C002E9027 /* Homebrew.swift */; };
@@ -1057,6 +1049,8 @@
0379C4A32ED7201D0035D7EA /* App+DetectApps.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App+DetectApps.swift"; sourceTree = "<group>"; }; 0379C4A32ED7201D0035D7EA /* App+DetectApps.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App+DetectApps.swift"; sourceTree = "<group>"; };
037F44152EDB0AA8002EBF75 /* FSNotifierTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FSNotifierTest.swift; sourceTree = "<group>"; }; 037F44152EDB0AA8002EBF75 /* FSNotifierTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FSNotifierTest.swift; sourceTree = "<group>"; };
037F44172EDB27B7002EBF75 /* Debouncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debouncer.swift; sourceTree = "<group>"; }; 037F44172EDB27B7002EBF75 /* Debouncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debouncer.swift; sourceTree = "<group>"; };
037F441C2EDB9195002EBF75 /* ConfigWatchManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigWatchManager.swift; sourceTree = "<group>"; };
037F44212EDB92EC002EBF75 /* HomebrewWatchManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewWatchManager.swift; sourceTree = "<group>"; };
0386B0B32ED36C3D00CA6795 /* Locked.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Locked.swift; sourceTree = "<group>"; }; 0386B0B32ED36C3D00CA6795 /* Locked.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Locked.swift; sourceTree = "<group>"; };
0386B0B82ED36DF800CA6795 /* LockedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockedTests.swift; sourceTree = "<group>"; }; 0386B0B82ED36DF800CA6795 /* LockedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockedTests.swift; sourceTree = "<group>"; };
0392CDE52EB23B8F009176DA /* CertificateValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateValidator.swift; sourceTree = "<group>"; }; 0392CDE52EB23B8F009176DA /* CertificateValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateValidator.swift; sourceTree = "<group>"; };
@@ -1175,7 +1169,6 @@
C44067F827E2585E0045BD4E /* DomainListTypeCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DomainListTypeCell.swift; sourceTree = "<group>"; }; C44067F827E2585E0045BD4E /* DomainListTypeCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DomainListTypeCell.swift; sourceTree = "<group>"; };
C44067FA27E25FD70045BD4E /* DomainListTLSCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DomainListTLSCell.swift; sourceTree = "<group>"; }; C44067FA27E25FD70045BD4E /* DomainListTLSCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DomainListTLSCell.swift; sourceTree = "<group>"; };
C4415E8C2B0287E90035F520 /* BrewFormulaeObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewFormulaeObservable.swift; sourceTree = "<group>"; }; C4415E8C2B0287E90035F520 /* BrewFormulaeObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewFormulaeObservable.swift; sourceTree = "<group>"; };
C441CC552AE8249400DDFACD /* ConfigFSNotifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigFSNotifier.swift; sourceTree = "<group>"; };
C44264BD2850B86C007400F1 /* SwiftUIHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIHelper.swift; sourceTree = "<group>"; }; C44264BD2850B86C007400F1 /* SwiftUIHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIHelper.swift; sourceTree = "<group>"; };
C44264BF2850BD2A007400F1 /* VersionPopoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionPopoverView.swift; sourceTree = "<group>"; }; C44264BF2850BD2A007400F1 /* VersionPopoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionPopoverView.swift; sourceTree = "<group>"; };
C4463FCB29804BCB007B93D5 /* RCFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RCFile.swift; sourceTree = "<group>"; }; C4463FCB29804BCB007B93D5 /* RCFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RCFile.swift; sourceTree = "<group>"; };
@@ -1246,7 +1239,6 @@
C49DA9BC2D67AC49006F9CF4 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = "<group>"; }; C49DA9BC2D67AC49006F9CF4 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = "<group>"; };
C49DA9BD2D67B298006F9CF4 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = "<group>"; }; C49DA9BD2D67B298006F9CF4 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = "<group>"; };
C49EAA5129B12A5A00AB28FC /* Measurements.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Measurements.swift; sourceTree = "<group>"; }; C49EAA5129B12A5A00AB28FC /* Measurements.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Measurements.swift; sourceTree = "<group>"; };
C49EAA5629B1689200AB28FC /* App+BrewWatch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App+BrewWatch.swift"; sourceTree = "<group>"; };
C4A81CA328C67101008DD9D1 /* PMTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PMTableView.swift; sourceTree = "<group>"; }; C4A81CA328C67101008DD9D1 /* PMTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PMTableView.swift; sourceTree = "<group>"; };
C4AC51FB27E27F47008528CA /* DomainListKindCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DomainListKindCell.swift; sourceTree = "<group>"; }; C4AC51FB27E27F47008528CA /* DomainListKindCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DomainListKindCell.swift; sourceTree = "<group>"; };
C4ACA38E25C754C100060C66 /* PhpExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpExtension.swift; sourceTree = "<group>"; }; C4ACA38E25C754C100060C66 /* PhpExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpExtension.swift; sourceTree = "<group>"; };
@@ -1282,8 +1274,6 @@
C4C3ED4227834C5200AB15D8 /* CustomPrefs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPrefs.swift; sourceTree = "<group>"; }; C4C3ED4227834C5200AB15D8 /* CustomPrefs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPrefs.swift; sourceTree = "<group>"; };
C4C8900228F0E28800CE5E97 /* FileSystemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileSystemProtocol.swift; sourceTree = "<group>"; }; C4C8900228F0E28800CE5E97 /* FileSystemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileSystemProtocol.swift; sourceTree = "<group>"; };
C4C8900428F0E3D100CE5E97 /* RealFileSystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealFileSystem.swift; sourceTree = "<group>"; }; C4C8900428F0E3D100CE5E97 /* RealFileSystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealFileSystem.swift; sourceTree = "<group>"; };
C4C8E817276F54D8003AC782 /* App+ConfigWatch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "App+ConfigWatch.swift"; sourceTree = "<group>"; };
C4C8E81A276F54E5003AC782 /* ConfigWatchManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigWatchManager.swift; sourceTree = "<group>"; };
C4CB250429B28BB800CA4492 /* MainMenuTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenuTest.swift; sourceTree = "<group>"; }; C4CB250429B28BB800CA4492 /* MainMenuTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenuTest.swift; sourceTree = "<group>"; };
C4CB6E64292C362C002E9027 /* Homebrew.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Homebrew.swift; sourceTree = "<group>"; }; C4CB6E64292C362C002E9027 /* Homebrew.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Homebrew.swift; sourceTree = "<group>"; };
C4CCBA6B275C567B008C7055 /* PMWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PMWindowController.swift; sourceTree = "<group>"; }; C4CCBA6B275C567B008C7055 /* PMWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PMWindowController.swift; sourceTree = "<group>"; };
@@ -2336,12 +2326,10 @@
C4C8E81D276F5686003AC782 /* Watcher */ = { C4C8E81D276F5686003AC782 /* Watcher */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
037F44172EDB27B7002EBF75 /* Debouncer.swift */,
C4C8E81A276F54E5003AC782 /* ConfigWatchManager.swift */,
C441CC552AE8249400DDFACD /* ConfigFSNotifier.swift */,
C41ADCE72970CCC700120423 /* FSNotifier.swift */, C41ADCE72970CCC700120423 /* FSNotifier.swift */,
C49EAA5629B1689200AB28FC /* App+BrewWatch.swift */, 037F44172EDB27B7002EBF75 /* Debouncer.swift */,
C4C8E817276F54D8003AC782 /* App+ConfigWatch.swift */, 037F441C2EDB9195002EBF75 /* ConfigWatchManager.swift */,
037F44212EDB92EC002EBF75 /* HomebrewWatchManager.swift */,
); );
path = Watcher; path = Watcher;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -2837,7 +2825,7 @@
0309E6672B0D4B2F002AC007 /* BrewExtensionsObservable.swift in Sources */, 0309E6672B0D4B2F002AC007 /* BrewExtensionsObservable.swift in Sources */,
C4E0F7ED27BEBDA9007475F2 /* NSWindowExtension.swift in Sources */, C4E0F7ED27BEBDA9007475F2 /* NSWindowExtension.swift in Sources */,
C4205A7E27F4D21800191A39 /* ValetProxy.swift in Sources */, C4205A7E27F4D21800191A39 /* ValetProxy.swift in Sources */,
C4C8E818276F54D8003AC782 /* App+ConfigWatch.swift in Sources */, 037F441F2EDB9195002EBF75 /* ConfigWatchManager.swift in Sources */,
C43B8FD52BA9BAD3000C02BE /* UnavailableContentView.swift in Sources */, C43B8FD52BA9BAD3000C02BE /* UnavailableContentView.swift in Sources */,
032DAC2A2E8BEB5B0018E01C /* RealWebApi.swift in Sources */, 032DAC2A2E8BEB5B0018E01C /* RealWebApi.swift in Sources */,
54FCFD30276C8DA4004CE748 /* HotkeyPreferenceView.swift in Sources */, 54FCFD30276C8DA4004CE748 /* HotkeyPreferenceView.swift in Sources */,
@@ -2850,6 +2838,7 @@
C4D5576429C77CC5001A44CD /* PhpVersionManagerWindowController.swift in Sources */, C4D5576429C77CC5001A44CD /* PhpVersionManagerWindowController.swift in Sources */,
C4E49DED28F764A00026AC4E /* TestableCommand.swift in Sources */, C4E49DED28F764A00026AC4E /* TestableCommand.swift in Sources */,
C41E871A2763D42300161EE0 /* DomainListVC+ContextMenu.swift in Sources */, C41E871A2763D42300161EE0 /* DomainListVC+ContextMenu.swift in Sources */,
037F44222EDB92EC002EBF75 /* HomebrewWatchManager.swift in Sources */,
C40C7F2827721FF600DDDCDC /* Valet+Alerts.swift in Sources */, C40C7F2827721FF600DDDCDC /* Valet+Alerts.swift in Sources */,
C463E380284930EE00422731 /* PresetHelper.swift in Sources */, C463E380284930EE00422731 /* PresetHelper.swift in Sources */,
C41C02A927E61A65009F26CB /* FakeValetSite.swift in Sources */, C41C02A927E61A65009F26CB /* FakeValetSite.swift in Sources */,
@@ -2938,9 +2927,7 @@
03DAD3A72EB3B08F003417BD /* DomainListVC+Certs.swift in Sources */, 03DAD3A72EB3B08F003417BD /* DomainListVC+Certs.swift in Sources */,
C46EBC4728DB9644007ACC74 /* RealShell.swift in Sources */, C46EBC4728DB9644007ACC74 /* RealShell.swift in Sources */,
C4068CAA27B0890D00544CD5 /* MenuBarIcons.swift in Sources */, C4068CAA27B0890D00544CD5 /* MenuBarIcons.swift in Sources */,
C441CC562AE8249400DDFACD /* ConfigFSNotifier.swift in Sources */,
C44264C02850BD2A007400F1 /* VersionPopoverView.swift in Sources */, C44264C02850BD2A007400F1 /* VersionPopoverView.swift in Sources */,
C4C8E81B276F54E5003AC782 /* ConfigWatchManager.swift in Sources */,
C417DC74277614690015E6EE /* Helpers.swift in Sources */, C417DC74277614690015E6EE /* Helpers.swift in Sources */,
C415D3E82770F692005EF286 /* AppDelegate+InterApp.swift in Sources */, C415D3E82770F692005EF286 /* AppDelegate+InterApp.swift in Sources */,
C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */, C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */,
@@ -2948,7 +2935,6 @@
C42759672627662800093CAE /* NSMenuExtension.swift in Sources */, C42759672627662800093CAE /* NSMenuExtension.swift in Sources */,
C422DDAA28A2C49900CEAC97 /* PhpDoctorView.swift in Sources */, C422DDAA28A2C49900CEAC97 /* PhpDoctorView.swift in Sources */,
C469E6FE294CF7B200A82AB2 /* FakeValetProxy.swift in Sources */, C469E6FE294CF7B200A82AB2 /* FakeValetProxy.swift in Sources */,
C490E3B629BCA367006D2DE6 /* App+BrewWatch.swift in Sources */,
C464ADAF275A7A69003FCD53 /* DomainListVC.swift in Sources */, C464ADAF275A7A69003FCD53 /* DomainListVC.swift in Sources */,
C44CCD4927AFF3B700CE40E5 /* MainMenu+Async.swift in Sources */, C44CCD4927AFF3B700CE40E5 /* MainMenu+Async.swift in Sources */,
C4C1019B27C65C6F001FACC2 /* Process.swift in Sources */, C4C1019B27C65C6F001FACC2 /* Process.swift in Sources */,
@@ -3147,8 +3133,6 @@
C471E87728F9BB650021E251 /* Keys.swift in Sources */, C471E87728F9BB650021E251 /* Keys.swift in Sources */,
C471E87828F9BB650021E251 /* TerminalProgressWindowController.swift in Sources */, C471E87828F9BB650021E251 /* TerminalProgressWindowController.swift in Sources */,
C471E87928F9BB650021E251 /* ProgressVC.swift in Sources */, C471E87928F9BB650021E251 /* ProgressVC.swift in Sources */,
C471E87B28F9BB650021E251 /* App+ConfigWatch.swift in Sources */,
C471E87C28F9BB650021E251 /* ConfigWatchManager.swift in Sources */,
C471E87D28F9BB650021E251 /* Preset.swift in Sources */, C471E87D28F9BB650021E251 /* Preset.swift in Sources */,
C471E87E28F9BB650021E251 /* PresetHelper.swift in Sources */, C471E87E28F9BB650021E251 /* PresetHelper.swift in Sources */,
C471E87F28F9BB650021E251 /* WarningView.swift in Sources */, C471E87F28F9BB650021E251 /* WarningView.swift in Sources */,
@@ -3179,6 +3163,7 @@
C4415E8F2B0287E90035F520 /* BrewFormulaeObservable.swift in Sources */, C4415E8F2B0287E90035F520 /* BrewFormulaeObservable.swift in Sources */,
C471E7D828F9BA8F0021E251 /* FileSystemProtocol.swift in Sources */, C471E7D828F9BA8F0021E251 /* FileSystemProtocol.swift in Sources */,
03BFF52F2E313244007F96FA /* StatusMenu+Driver.swift in Sources */, 03BFF52F2E313244007F96FA /* StatusMenu+Driver.swift in Sources */,
037F44242EDB92EC002EBF75 /* HomebrewWatchManager.swift in Sources */,
C471E7F328F9BAC70021E251 /* PhpHelper.swift in Sources */, C471E7F328F9BAC70021E251 /* PhpHelper.swift in Sources */,
C46DC7A62C7B5BC900F19D17 /* Favorites.swift in Sources */, C46DC7A62C7B5BC900F19D17 /* Favorites.swift in Sources */,
C471E7E728F9BAC20021E251 /* Constants.swift in Sources */, C471E7E728F9BAC20021E251 /* Constants.swift in Sources */,
@@ -3201,7 +3186,6 @@
C40D72612A018AE30054A067 /* BrewFormula+UI.swift in Sources */, C40D72612A018AE30054A067 /* BrewFormula+UI.swift in Sources */,
C471E82528F9BB2E0021E251 /* ComposerWindow.swift in Sources */, C471E82528F9BB2E0021E251 /* ComposerWindow.swift in Sources */,
0396160D2E74A61E002DD7F6 /* LoggableEvent.swift in Sources */, 0396160D2E74A61E002DD7F6 /* LoggableEvent.swift in Sources */,
C441CC582AE8249400DDFACD /* ConfigFSNotifier.swift in Sources */,
C471E80828F9BAD40021E251 /* PhpExtension.swift in Sources */, C471E80828F9BAD40021E251 /* PhpExtension.swift in Sources */,
C471E7F928F9BACB0021E251 /* PhpSwitcher.swift in Sources */, C471E7F928F9BACB0021E251 /* PhpSwitcher.swift in Sources */,
03ACC6482ECCBA130070D4CD /* CaskFile+API.swift in Sources */, 03ACC6482ECCBA130070D4CD /* CaskFile+API.swift in Sources */,
@@ -3209,8 +3193,8 @@
031E2B6B2B1525A7007C29E1 /* BrewPhpExtension.swift in Sources */, 031E2B6B2B1525A7007C29E1 /* BrewPhpExtension.swift in Sources */,
C471E82728F9BB310021E251 /* BrewDiagnostics.swift in Sources */, C471E82728F9BB310021E251 /* BrewDiagnostics.swift in Sources */,
C471E7DB28F9BA8F0021E251 /* RealShell.swift in Sources */, C471E7DB28F9BA8F0021E251 /* RealShell.swift in Sources */,
C490E3B929BCA368006D2DE6 /* App+BrewWatch.swift in Sources */,
C471E7FF28F9BAD10021E251 /* Xdebug.swift in Sources */, C471E7FF28F9BAD10021E251 /* Xdebug.swift in Sources */,
037F441D2EDB9195002EBF75 /* ConfigWatchManager.swift in Sources */,
C409349F298EE8E900D25014 /* AppUpdater.swift in Sources */, C409349F298EE8E900D25014 /* AppUpdater.swift in Sources */,
03BFF5292E312C3D007F96FA /* Startup+Timers.swift in Sources */, 03BFF5292E312C3D007F96FA /* Startup+Timers.swift in Sources */,
C471E7F228F9BAC70021E251 /* PhpEnvironments.swift in Sources */, C471E7F228F9BAC70021E251 /* PhpEnvironments.swift in Sources */,
@@ -3272,7 +3256,6 @@
036C39022E5C883B008DAEDF /* Packagist.swift in Sources */, 036C39022E5C883B008DAEDF /* Packagist.swift in Sources */,
0379C4A72ED720220035D7EA /* App+DetectApps.swift in Sources */, 0379C4A72ED720220035D7EA /* App+DetectApps.swift in Sources */,
C471E89428F9BB8F0021E251 /* LocalNotification.swift in Sources */, C471E89428F9BB8F0021E251 /* LocalNotification.swift in Sources */,
C441CC592AE8249400DDFACD /* ConfigFSNotifier.swift in Sources */,
C40934A5298EEB2C00D25014 /* CaskFile.swift in Sources */, C40934A5298EEB2C00D25014 /* CaskFile.swift in Sources */,
C471E89528F9BB8F0021E251 /* MenuBarImageGenerator.swift in Sources */, C471E89528F9BB8F0021E251 /* MenuBarImageGenerator.swift in Sources */,
C40D725D2A018ACC0054A067 /* BusyStatus.swift in Sources */, C40D725D2A018ACC0054A067 /* BusyStatus.swift in Sources */,
@@ -3375,8 +3358,6 @@
C471E8DC28F9BB8F0021E251 /* ProgressVC.swift in Sources */, C471E8DC28F9BB8F0021E251 /* ProgressVC.swift in Sources */,
03D846262EB6344E006EFE3C /* DomainListVC+Window.swift in Sources */, 03D846262EB6344E006EFE3C /* DomainListVC+Window.swift in Sources */,
C490E3BF29BCA376006D2DE6 /* Measurements.swift in Sources */, C490E3BF29BCA376006D2DE6 /* Measurements.swift in Sources */,
C471E8DE28F9BB8F0021E251 /* App+ConfigWatch.swift in Sources */,
C471E8DF28F9BB8F0021E251 /* ConfigWatchManager.swift in Sources */,
C4CB250529B28BB800CA4492 /* MainMenuTest.swift in Sources */, C4CB250529B28BB800CA4492 /* MainMenuTest.swift in Sources */,
C40D72622A018AE30054A067 /* BrewFormula+UI.swift in Sources */, C40D72622A018AE30054A067 /* BrewFormula+UI.swift in Sources */,
C4B79ECE29CA475900A483EE /* RemovePhpVersionCommand.swift in Sources */, C4B79ECE29CA475900A483EE /* RemovePhpVersionCommand.swift in Sources */,
@@ -3398,6 +3379,7 @@
C456A0CE2AA6166F0080144F /* BytePhpPreference.swift in Sources */, C456A0CE2AA6166F0080144F /* BytePhpPreference.swift in Sources */,
C4FD87A829AB9ABD0002D701 /* PhpConfigChecker.swift in Sources */, C4FD87A829AB9ABD0002D701 /* PhpConfigChecker.swift in Sources */,
C45B9151295608E300F4EC78 /* ValetServicesManager.swift in Sources */, C45B9151295608E300F4EC78 /* ValetServicesManager.swift in Sources */,
037F44202EDB9195002EBF75 /* ConfigWatchManager.swift in Sources */,
C47015032C46D7F00069AAE7 /* NVAlertExtension.swift in Sources */, C47015032C46D7F00069AAE7 /* NVAlertExtension.swift in Sources */,
C471E8EC28F9BB8F0021E251 /* SwiftUIHelper.swift in Sources */, C471E8EC28F9BB8F0021E251 /* SwiftUIHelper.swift in Sources */,
C471E8EE28F9BB8F0021E251 /* HotKey.swift in Sources */, C471E8EE28F9BB8F0021E251 /* HotKey.swift in Sources */,
@@ -3419,7 +3401,6 @@
C4611E5A2AEAD2E20010BE24 /* ConfigManagerWindowController.swift in Sources */, C4611E5A2AEAD2E20010BE24 /* ConfigManagerWindowController.swift in Sources */,
C471E80E28F9BAE80021E251 /* DateExtension.swift in Sources */, C471E80E28F9BAE80021E251 /* DateExtension.swift in Sources */,
036C390F2E5C8D42008DAEDF /* PackagistError.swift in Sources */, 036C390F2E5C8D42008DAEDF /* PackagistError.swift in Sources */,
C490E3BA29BCA368006D2DE6 /* App+BrewWatch.swift in Sources */,
03BFF5272E312C3D007F96FA /* Startup+Timers.swift in Sources */, 03BFF5272E312C3D007F96FA /* Startup+Timers.swift in Sources */,
C471E7D028F9BA630021E251 /* FileSystemProtocol.swift in Sources */, C471E7D028F9BA630021E251 /* FileSystemProtocol.swift in Sources */,
C471E81228F9BAE80021E251 /* TimeIntervalExtension.swift in Sources */, C471E81228F9BAE80021E251 /* TimeIntervalExtension.swift in Sources */,
@@ -3435,6 +3416,7 @@
C471E7F828F9BACB0021E251 /* InternalSwitcher.swift in Sources */, C471E7F828F9BACB0021E251 /* InternalSwitcher.swift in Sources */,
C471E82328F9BB2E0021E251 /* ComposerJson.swift in Sources */, C471E82328F9BB2E0021E251 /* ComposerJson.swift in Sources */,
C471E82128F9BB2E0021E251 /* ProjectTypeDetection.swift in Sources */, C471E82128F9BB2E0021E251 /* ProjectTypeDetection.swift in Sources */,
037F44232EDB92EC002EBF75 /* HomebrewWatchManager.swift in Sources */,
037F441A2EDB27BA002EBF75 /* Debouncer.swift in Sources */, 037F441A2EDB27BA002EBF75 /* Debouncer.swift in Sources */,
032DAC282E8BEB5B0018E01C /* RealWebApi.swift in Sources */, 032DAC282E8BEB5B0018E01C /* RealWebApi.swift in Sources */,
C471E7EF28F9BAC30021E251 /* Actions.swift in Sources */, C471E7EF28F9BAC30021E251 /* Actions.swift in Sources */,
@@ -3548,7 +3530,6 @@
C43A8A2425D9D20D00591B77 /* HomebrewPackageTest.swift in Sources */, C43A8A2425D9D20D00591B77 /* HomebrewPackageTest.swift in Sources */,
C485707928BF456C00539B36 /* ArrayExtension.swift in Sources */, C485707928BF456C00539B36 /* ArrayExtension.swift in Sources */,
C4F780CA25D80B75000DBC97 /* HomebrewDecodable.swift in Sources */, C4F780CA25D80B75000DBC97 /* HomebrewDecodable.swift in Sources */,
C4C8E81C276F54E5003AC782 /* ConfigWatchManager.swift in Sources */,
C4F319C927B034A500AFF46F /* Stats.swift in Sources */, C4F319C927B034A500AFF46F /* Stats.swift in Sources */,
C4F30B04278E16BA00755FCE /* HomebrewService.swift in Sources */, C4F30B04278E16BA00755FCE /* HomebrewService.swift in Sources */,
54D9E0B527E4F51E003B9AD9 /* Key.swift in Sources */, 54D9E0B527E4F51E003B9AD9 /* Key.swift in Sources */,
@@ -3567,7 +3548,6 @@
0392CDE92EB23B8F009176DA /* CertificateValidator.swift in Sources */, 0392CDE92EB23B8F009176DA /* CertificateValidator.swift in Sources */,
C4821C5B2C2DEDE200357A68 /* AppMenu.swift in Sources */, C4821C5B2C2DEDE200357A68 /* AppMenu.swift in Sources */,
C463E381284930EE00422731 /* PresetHelper.swift in Sources */, C463E381284930EE00422731 /* PresetHelper.swift in Sources */,
C441CC572AE8249400DDFACD /* ConfigFSNotifier.swift in Sources */,
C4F520672AF03791006787F2 /* ExtensionEnumeratorTest.swift in Sources */, C4F520672AF03791006787F2 /* ExtensionEnumeratorTest.swift in Sources */,
C46FA98C2822F08F00D78807 /* PhpConfigurationFileTest.swift in Sources */, C46FA98C2822F08F00D78807 /* PhpConfigurationFileTest.swift in Sources */,
C4D5576529C77CC5001A44CD /* PhpVersionManagerWindowController.swift in Sources */, C4D5576529C77CC5001A44CD /* PhpVersionManagerWindowController.swift in Sources */,
@@ -3582,7 +3562,6 @@
C42106672AFA9FF400DF3732 /* PhpVersionManagerView+Actions.swift in Sources */, C42106672AFA9FF400DF3732 /* PhpVersionManagerView+Actions.swift in Sources */,
C46DC7A52C7B5BC900F19D17 /* Favorites.swift in Sources */, C46DC7A52C7B5BC900F19D17 /* Favorites.swift in Sources */,
032DAC2F2E8BEB6B0018E01C /* WebApiProtocol.swift in Sources */, 032DAC2F2E8BEB6B0018E01C /* WebApiProtocol.swift in Sources */,
C4C8E819276F54D8003AC782 /* App+ConfigWatch.swift in Sources */,
C4FC21B128391F8E00D368BB /* MainMenu+Actions.swift in Sources */, C4FC21B128391F8E00D368BB /* MainMenu+Actions.swift in Sources */,
54D9E0B927E4F51E003B9AD9 /* KeyCombo.swift in Sources */, 54D9E0B927E4F51E003B9AD9 /* KeyCombo.swift in Sources */,
C4EED88A27A48778006D7272 /* InterAppHandler.swift in Sources */, C4EED88A27A48778006D7272 /* InterAppHandler.swift in Sources */,
@@ -3597,12 +3576,14 @@
C4E2E85D28FC282B003B070C /* TestableConfiguration.swift in Sources */, C4E2E85D28FC282B003B070C /* TestableConfiguration.swift in Sources */,
C485706E28BF451C00539B36 /* OnboardingWindowController.swift in Sources */, C485706E28BF451C00539B36 /* OnboardingWindowController.swift in Sources */,
C4BB393A2981AFC700F8E797 /* PhpVersionSource.swift in Sources */, C4BB393A2981AFC700F8E797 /* PhpVersionSource.swift in Sources */,
037F44252EDB92EC002EBF75 /* HomebrewWatchManager.swift in Sources */,
C4CB6E66292C362C002E9027 /* Homebrew.swift in Sources */, C4CB6E66292C362C002E9027 /* Homebrew.swift in Sources */,
C43603A1275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */, C43603A1275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */,
C4C3643A28AE4FCE00C0770E /* StatusMenu+Items.swift in Sources */, C4C3643A28AE4FCE00C0770E /* StatusMenu+Items.swift in Sources */,
C42759682627662800093CAE /* NSMenuExtension.swift in Sources */, C42759682627662800093CAE /* NSMenuExtension.swift in Sources */,
03BFF52D2E313244007F96FA /* StatusMenu+Driver.swift in Sources */, 03BFF52D2E313244007F96FA /* StatusMenu+Driver.swift in Sources */,
C4AFC4B429C4F43300BF4E0D /* HomebrewUpgradableTest.swift in Sources */, C4AFC4B429C4F43300BF4E0D /* HomebrewUpgradableTest.swift in Sources */,
037F441E2EDB9195002EBF75 /* ConfigWatchManager.swift in Sources */,
C4E2E84828FC1D93003B070C /* TestableConfigurationTest.swift in Sources */, C4E2E84828FC1D93003B070C /* TestableConfigurationTest.swift in Sources */,
C4D936CB27E3EE4A00BD69FE /* DomainListCellProtocol.swift in Sources */, C4D936CB27E3EE4A00BD69FE /* DomainListCellProtocol.swift in Sources */,
C4513F962B13E30C001AD760 /* BrewExtensionsObservable.swift in Sources */, C4513F962B13E30C001AD760 /* BrewExtensionsObservable.swift in Sources */,
@@ -3694,7 +3675,6 @@
033D45992B0D4EC600070080 /* InstallPhpExtensionCommand.swift in Sources */, 033D45992B0D4EC600070080 /* InstallPhpExtensionCommand.swift in Sources */,
C4F5FBCD28218CB8001065C5 /* Xdebug.swift in Sources */, C4F5FBCD28218CB8001065C5 /* Xdebug.swift in Sources */,
C40B24F227A310770018C7D2 /* Events.swift in Sources */, C40B24F227A310770018C7D2 /* Events.swift in Sources */,
C490E3B829BCA367006D2DE6 /* App+BrewWatch.swift in Sources */,
C44AD3F72912EF7100997FF4 /* RealFileSystemTest.swift in Sources */, C44AD3F72912EF7100997FF4 /* RealFileSystemTest.swift in Sources */,
C4F30B0A278E1A1A00755FCE /* ComposerJson.swift in Sources */, C4F30B0A278E1A1A00755FCE /* ComposerJson.swift in Sources */,
C4C0E8E027F88AEB002D32A9 /* FakeDomainScanner.swift in Sources */, C4C0E8E027F88AEB002D32A9 /* FakeDomainScanner.swift in Sources */,

View File

@@ -136,15 +136,16 @@ class App {
// MARK: - App Watchers // MARK: - App Watchers
/** Individual filesystem watchers, which are, i.e. responsible for watching the Homebrew folders. */
var watchers: [String: FSNotifier] = [:]
/** Individual debouncers for filesystem watchers. */
var debouncers: [String: Debouncer] = [:]
/** /**
The `ConfigWatchManager` is responsible for watching the `.ini` files and the `.conf.d` folder. The `ConfigWatchManager` is responsible for watching the `.ini` files and the `.conf.d` folder.
This manager object can immediately start or stop all watchers (or pause them) all at once. This manager object can immediately start or stop all watchers (or pause them) all at once.
*/ */
var watchManager: ConfigWatchManager! var configWatchManager: ConfigWatchManager?
/**
The `HomebrewWatchManager` is responsible for watching the Homebrew binaries folder.
This allows PHP Monitor to respond to external `brew` changes executed by the user.
*/
var homebrewWatchManager: HomebrewWatchManager?
} }

View File

@@ -68,13 +68,13 @@ extension Startup {
await container.phpEnvs.reloadPhpVersions() await container.phpEnvs.reloadPhpVersions()
// Set up the filesystem watcher for the Homebrew binaries // Set up the filesystem watcher for the Homebrew binaries
App.shared.prepareHomebrewWatchers() await HomebrewWatchManager.prepare()
// Check for other problems // Check for other problems
container.warningManager.evaluateWarnings() container.warningManager.evaluateWarnings()
// Set up the config watchers on launch (updated automatically when switching) // Set up the config watchers on launch (updated automatically when switching)
App.shared.handlePhpConfigWatcher() await ConfigWatchManager.handleWatcher()
// Detect built-in and custom applications // Detect built-in and custom applications
await App.shared.detectApplications() await App.shared.detectApplications()

View File

@@ -292,7 +292,7 @@ extension MainMenu {
await PhpEnvironments.switcher.performSwitch(to: version) await PhpEnvironments.switcher.performSwitch(to: version)
container.phpEnvs.currentInstall = ActivePhpInstallation(container) container.phpEnvs.currentInstall = ActivePhpInstallation(container)
App.shared.handlePhpConfigWatcher() await ConfigWatchManager.handleWatcher()
container.phpEnvs.delegate?.switcherDidCompleteSwitch(to: version) container.phpEnvs.delegate?.switcherDidCompleteSwitch(to: version)
} }
@@ -307,7 +307,7 @@ extension MainMenu {
await PhpEnvironments.switcher.performSwitch(to: version) await PhpEnvironments.switcher.performSwitch(to: version)
container.phpEnvs.currentInstall = ActivePhpInstallation(container) container.phpEnvs.currentInstall = ActivePhpInstallation(container)
App.shared.handlePhpConfigWatcher() await ConfigWatchManager.handleWatcher()
container.phpEnvs.delegate?.switcherDidCompleteSwitch(to: version) container.phpEnvs.delegate?.switcherDidCompleteSwitch(to: version)
} }
} }
@@ -333,7 +333,7 @@ extension MainMenu {
await PhpEnvironments.switcher.performSwitch(to: version) await PhpEnvironments.switcher.performSwitch(to: version)
container.phpEnvs.currentInstall = ActivePhpInstallation(container) container.phpEnvs.currentInstall = ActivePhpInstallation(container)
App.shared.handlePhpConfigWatcher() await ConfigWatchManager.handleWatcher()
container.phpEnvs.delegate?.switcherDidCompleteSwitch(to: version) container.phpEnvs.delegate?.switcherDidCompleteSwitch(to: version)
} }

View File

@@ -1,61 +0,0 @@
//
// App+BrewWatch.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 03/03/2023.
// Copyright © 2025 Nico Verbruggen. All rights reserved.
//
import Foundation
extension App {
/**
Performs a particular action while suspending the Homebrew watcher,
until the task is completed.
*/
public func withSuspendedHomebrewWatcher<T>(_ action: () async throws -> T) async rethrows -> T {
await suspendHomebrewWatcher()
defer { resumeHomebrewWatcher() }
return try await action()
}
/**
Prepares the `homebrew/bin` directory watcher. This allows PHP Monitor to quickly respond to
external `brew` changes executed by the user.
*/
public func prepareHomebrewWatchers() {
let notifier = FSNotifier(
for: URL(fileURLWithPath: container.paths.binPath),
eventMask: .all,
onChange: { Task { await self.onHomebrewPhpModification() } }
)
self.watchers["homebrewBinaries"] = notifier
self.debouncers["homebrewBinaries"] = Debouncer()
}
private func suspendHomebrewWatcher() async {
watchers["homebrewBinaries"]?.suspend()
await debouncers["homebrewBinaries"]?.cancel()
}
private func resumeHomebrewWatcher() {
watchers["homebrewBinaries"]?.resume()
}
public func onHomebrewPhpModification() async {
if let debouncer = self.debouncers["homebrewBinaries"] {
await debouncer.debounce(for: 5.0) {
Log.info("No changes in `\(self.container.paths.binPath)` occurred for 5 seconds. Reloading now.")
// We reload the PHP versions in the background
await self.container.phpEnvs.reloadPhpVersions()
// Finally, refresh the active installation
await MainMenu.shared.refreshActiveInstallation()
}
}
}
}

View File

@@ -1,63 +0,0 @@
//
// App+ConfigWatch.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 30/03/2021.
// Copyright © 2025 Nico Verbruggen. All rights reserved.
//
import Foundation
extension App {
func startWatchManager(_ url: URL) {
Log.perf("Starting config watch manager...")
self.watchManager = ConfigWatchManager(for: url)
self.watchManager.didChange = { url in
Log.perf("Something has changed in: \(url)")
// Check if the watcher has last updated the menu less than 0.75s ago
let distance = self.watchManager.lastUpdate?.distance(to: Date().timeIntervalSince1970)
if distance == nil || distance != nil && distance! > 0.75 {
Log.perf("Refreshing menu...")
Task { @MainActor in MainMenu.shared.reloadPhpMonitorMenuInBackground() }
self.watchManager.lastUpdate = Date().timeIntervalSince1970
}
}
}
func handlePhpConfigWatcher(forceReload: Bool = false) {
if container.filesystem is TestableFileSystem {
Log.warn("Config watch manager is disabled when using testable filesystem.")
return
}
guard let install = container.phpEnvs.phpInstall else {
Log.info("It appears as if no PHP installation is currently active.")
Log.info("The config watch manager be disabled until a PHP install is active.")
return
}
let url = URL(fileURLWithPath: "\(container.paths.etcPath)/php/\(install.version.short)")
// Check whether the manager exists and schedule on the main thread
// if we don't consistently do this, the app will create duplicate watchers
// due to timing issues, which creates retain cycles
Task { @MainActor in
// Watcher needs to be created
if self.watchManager == nil {
self.startWatchManager(url)
}
// Watcher needs to be updated
if self.watchManager.url != url || forceReload {
self.watchManager.disable()
self.watchManager = nil
Log.perf("Watcher has stopped watching files. Starting new one...")
self.startWatchManager(url)
}
}
}
}

View File

@@ -1,89 +0,0 @@
//
// ConfigFSNotifier.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 24/10/2023.
// Copyright © 2025 Nico Verbruggen. All rights reserved.
//
import Foundation
class ConfigFSNotifier {
enum Behaviour {
case reloadsMenu
case reloadsWatchers
}
let url: URL
private var parent: ConfigWatchManager!
private var monitoredFolderFileDescriptor: CInt = -1
private var folderMonitorSource: DispatchSourceFileSystemObject?
init(
for url: URL,
eventMask: DispatchSource.FileSystemEvent,
parent: ConfigWatchManager,
behaviour: ConfigFSNotifier.Behaviour = .reloadsMenu
) {
self.url = url
self.parent = parent
self.startMonitoring(eventMask, behaviour: behaviour)
}
func startMonitoring(
_ eventMask: DispatchSource.FileSystemEvent,
behaviour: ConfigFSNotifier.Behaviour
) {
// Ensure our starting state is correct, we may already be monitoring!
guard folderMonitorSource == nil && monitoredFolderFileDescriptor == -1 else {
return
}
// We'll try to open a file descriptor and validate it
monitoredFolderFileDescriptor = open(url.path, O_EVTONLY)
// If our file descriptor here is still -1, there may have been an issue and we abort
guard monitoredFolderFileDescriptor >= 0 else {
Log.err("Failed to open file descriptor for \(url.path), not monitoring.")
return
}
// Set the source (with proper file descriptor, event mask and using the right queue)
folderMonitorSource = DispatchSource.makeFileSystemObjectSource(
fileDescriptor: monitoredFolderFileDescriptor,
eventMask: eventMask,
queue: parent.queue
)
// Set the event handler (fires depending on the event mask)
folderMonitorSource?.setEventHandler { [weak self] in
if behaviour == .reloadsWatchers
&& !ConfigWatchManager.ignoresModificationsToConfigValues {
// Reload all configuration watchers
return App.shared.handlePhpConfigWatcher(forceReload: true)
}
if let url = self?.url {
self?.parent.didChange?(url)
}
}
// Cancellation handler, fired when we stop monitoring files
folderMonitorSource?.setCancelHandler { [weak self] in
guard let self = self else { return }
close(self.monitoredFolderFileDescriptor)
self.monitoredFolderFileDescriptor = -1
self.folderMonitorSource = nil
}
folderMonitorSource?.resume()
}
func stopMonitoring() {
folderMonitorSource?.cancel()
self.parent = nil
}
}

View File

@@ -8,24 +8,71 @@
import Foundation import Foundation
class ConfigWatchManager { actor ConfigWatchManager {
enum Behaviour {
case reloadsMenu
case reloadsWatchers
}
// MARK: Global state (applicable to ALL watchers) // MARK: Global state (applicable to ALL watchers)
static var ignoresModificationsToConfigValues: Bool = false static var ignoresModificationsToConfigValues: Bool = false
// MARK: Public variables // MARK: Static methods
private var watchers: [ConfigFSNotifier] = [] /**
Handles the PHP config watcher lifecycle. Creates a new watcher if needed,
or recreates it if the PHP version has changed.
let queue = DispatchQueue(label: "com.nicoverbruggen.phpmon.config_watch") Actor isolation ensures no duplicate watchers or retain cycles.
let url: URL */
var lastUpdate: TimeInterval? @MainActor
var didChange: ((URL) -> Void)? public static func handleWatcher(forceReload: Bool = false) async {
let container = App.shared.container
if container.filesystem is TestableFileSystem {
Log.warn("Config watch manager is disabled when using testable filesystem.")
return
}
guard let install = container.phpEnvs.phpInstall else {
Log.info("It appears as if no PHP installation is currently active.")
Log.info("The config watch manager is disabled until a PHP install is active.")
return
}
let url = URL(fileURLWithPath: "\(container.paths.etcPath)/php/\(install.version.short)")
// Create watcher if missing
guard let manager = App.shared.configWatchManager else {
let manager = ConfigWatchManager(for: url)
await manager.setupWatchers()
App.shared.configWatchManager = manager
return
}
// Update existing watcher if needed
if await manager.url != url {
// URL changed - update to different PHP version
await manager.updateUrl(to: url)
} else if forceReload {
// Same URL - just reload watchers (e.g., conf.d files added/removed)
await manager.reloadWatchers()
}
}
// MARK: Instance variables
private var watchers: [FSNotifier] = []
private var debouncer: Debouncer
private(set) var url: URL
nonisolated private let debounceInterval: TimeInterval
// MARK: Methods // MARK: Methods
init(for url: URL) { init(for url: URL, debounceInterval: TimeInterval = 0.75) {
if App.shared.container.filesystem is TestableFileSystem { if App.shared.container.filesystem is TestableFileSystem {
fatalError(""" fatalError("""
ConfigWatchManager is currently incompatible with a testable filesystem!" ConfigWatchManager is currently incompatible with a testable filesystem!"
@@ -34,6 +81,13 @@ class ConfigWatchManager {
} }
self.url = url self.url = url
self.debounceInterval = debounceInterval
self.debouncer = Debouncer()
}
func setupWatchers() {
// Guard against double setup
assert(watchers.isEmpty, "setupWatchers() called when watchers already exist")
// Add a watcher for php.ini // Add a watcher for php.ini
self.addWatcher(for: self.url.appendingPathComponent("php.ini"), eventMask: .write) self.addWatcher(for: self.url.appendingPathComponent("php.ini"), eventMask: .write)
@@ -62,25 +116,64 @@ class ConfigWatchManager {
})) }))
} }
func addWatcher( private func clearWatchers() {
for watcher in self.watchers {
watcher.terminate()
}
self.watchers.removeAll()
}
func reloadWatchers() {
Log.perf("Reloading configuration watchers...")
clearWatchers()
setupWatchers()
}
func updateUrl(to newUrl: URL) {
Log.perf("Updating watcher URL from \(self.url.path) to \(newUrl.path)...")
clearWatchers()
self.url = newUrl
setupWatchers()
}
private func handleConfigChange(at url: URL) async {
await debouncer.debounce(for: debounceInterval) {
Log.perf("Config file changed at \(url.path), debounce completed. Refreshing menu...")
Task { @MainActor in MainMenu.shared.reloadPhpMonitorMenuInBackground() }
}
}
private func addWatcher(
for url: URL, for url: URL,
eventMask: DispatchSource.FileSystemEvent, eventMask: DispatchSource.FileSystemEvent,
behaviour: ConfigFSNotifier.Behaviour = .reloadsMenu behaviour: Behaviour = .reloadsMenu
) { ) {
if !App.shared.container.filesystem.anyExists(url.path) { if !App.shared.container.filesystem.anyExists(url.path) {
Log.warn("No watcher was created for \(url.path) because the requested file does not exist.") Log.warn("No watcher was created for \(url.path) because the requested file does not exist.")
return return
} }
let watcher = ConfigFSNotifier(for: url, eventMask: eventMask, parent: self, behaviour: behaviour) let watcher = FSNotifier(for: url, eventMask: eventMask) { [weak self] in
guard let self = self else { return }
Task {
if behaviour == .reloadsWatchers
&& !ConfigWatchManager.ignoresModificationsToConfigValues {
// Reload all configuration watchers on this manager
await self.reloadWatchers()
return
}
await self.handleConfigChange(at: url)
}
}
self.watchers.append(watcher) self.watchers.append(watcher)
} }
func disable() { func disable() async {
Log.perf("Turning off all individual existing watchers...") Log.perf("Turning off all individual existing watchers...")
self.watchers.forEach { (watcher) in await debouncer.cancel()
watcher.stopMonitoring() clearWatchers()
}
} }
deinit { deinit {

View File

@@ -8,26 +8,37 @@
import Foundation import Foundation
class FSNotifier { actor FSNotifier {
public static var shared: FSNotifier! = nil // MARK: Variables
// MARK: Public variables /** The URL of the file or folder that is being observed. */
nonisolated let url: URL
let queue = DispatchQueue(label: "com.nicoverbruggen.phpmon.fs_notifier") /** Whether responding to events is currently on hold. */
let url: URL private(set) var isSuspended = false
// MARK: Private variables // MARK: Internal Variables
private var fileDescriptor: CInt = -1 /** The queue that is used for the `dispatchSource`. */
private var dispatchSource: DispatchSourceFileSystemObject? private nonisolated let queue: DispatchQueue
private var isSuspended = false /** An open file or folder required for observation. */
private nonisolated(unsafe) var fileDescriptor: CInt = -1
/** A dispatch source that monitors events associated with a file or folder. */
private nonisolated(unsafe) var dispatchSource: DispatchSourceFileSystemObject?
// MARK: Methods // MARK: Methods
init(for url: URL, eventMask: DispatchSource.FileSystemEvent, onChange: @escaping () -> Void) { init(
for url: URL,
eventMask: DispatchSource.FileSystemEvent,
queue: DispatchQueue? = nil,
onChange: @escaping () -> Void
) {
self.url = url self.url = url
self.queue = queue ?? DispatchQueue(label: "com.nicoverbruggen.phpmon.fs_notifier")
fileDescriptor = open(url.path, O_EVTONLY) fileDescriptor = open(url.path, O_EVTONLY)
@@ -42,19 +53,19 @@ class FSNotifier {
queue: self.queue queue: self.queue
) )
dispatchSource?.setEventHandler(handler: { dispatchSource?.setEventHandler(handler: { [weak self] in
self.queue.async { Task { [weak self] in
guard let self = self else { return }
// If our notifier is suspended, don't fire // If our notifier is suspended, don't fire
guard !self.isSuspended else { return } guard await !self.isSuspended else { return }
// If our notifier is not suspended, fire // If our notifier is not suspended, fire
Task { onChange() } onChange()
} }
}) })
dispatchSource?.setCancelHandler(handler: { [weak self] in dispatchSource?.setCancelHandler(handler: {
guard let self = self else { return }
close(self.fileDescriptor) close(self.fileDescriptor)
self.fileDescriptor = -1 self.fileDescriptor = -1
self.dispatchSource = nil self.dispatchSource = nil
@@ -63,26 +74,25 @@ class FSNotifier {
dispatchSource?.resume() dispatchSource?.resume()
} }
func suspend() { /** Suspends responding to filesystem events. This does not stop events from being observed! */
self.queue.async { func suspend() async {
self.isSuspended = true self.isSuspended = true
Log.perf("FSNotifier for \(self.url) has been suspended.") Log.perf("FSNotifier for \(self.url.path) has been suspended.")
}
} }
func resume() { /** Resumes responding to filesystem events. */
self.queue.async { func resume() async {
self.isSuspended = false self.isSuspended = false
Log.perf("FSNotifier for \(self.url) has been resumed.") Log.perf("FSNotifier for \(self.url.path) has been resumed.")
}
} }
func terminate() { /** Terminates the file monitor, which will cause `deinit` to fire. */
nonisolated func terminate() {
dispatchSource?.cancel() dispatchSource?.cancel()
} }
deinit { nonisolated deinit {
Log.perf("FSNotifier for \(self.url) will be deinitialized.") Log.perf("deinit: FSNotifier @ \(self.url.path)")
} }
} }

View File

@@ -0,0 +1,173 @@
//
// HomebrewWatchManager.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 29/11/2025.
// Copyright © 2025 Nico Verbruggen. All rights reserved.
//
import Foundation
actor HomebrewWatchManager {
// MARK: Public API
/**
Prepares the Homebrew watcher. This allows PHP Monitor to quickly respond to
external `brew` changes executed by the user.
*/
@MainActor
public static func prepare() async {
let binPath = App.shared.container.paths.binPath
let manager = HomebrewWatchManager(
for: URL(fileURLWithPath: binPath),
debounceInterval: 5.0
)
await manager.setupWatcher()
App.shared.homebrewWatchManager = manager
}
/**
Performs a particular action while suspending the Homebrew watcher,
until the task is completed.
Any operations that cause Homebrew to perform tasks (installing,
updating, removing packages) should be wrapped in this helper method,
to prevent the app from doing duplicate work.
*/
public static func withSuspended<T>(_ action: () async throws -> T) async rethrows -> T {
guard let manager = App.shared.homebrewWatchManager else {
// If there's no manager, run the task as-is
return try await action()
}
// Suspend, execute the action, and resume
return try await manager.withSuspended(action)
}
// MARK: - Instance variables
/**
The underlying `FSNotifier` which will respond to filesystem events.
*/
private var watcher: FSNotifier?
/**
The debouncer, responsible for ensuring events stop firing before
finally responding to changes in `homebrew/bin`.
*/
private var debouncer: Debouncer
/**
The URL of the `homebrew/bin` path, that we will be watching, too.
*/
nonisolated let url: URL
/**
The interval for the debounce. Prevents bulk changes from triggering
too many fired events.
*/
nonisolated let debounceInterval: TimeInterval
// MARK: - Lifecycle
init(for url: URL, debounceInterval: TimeInterval = 5.0) {
if App.shared.container.filesystem is TestableFileSystem {
fatalError("""
HomebrewWatchManager is currently incompatible with a testable filesystem!
You are not allowed to instantiate these while using a testable filesystem.
""")
}
self.url = url
self.debounceInterval = debounceInterval
self.debouncer = Debouncer()
}
deinit {
Log.perf("deinit: \(String(describing: self)).\(#function)")
}
// MARK: - Internal Methods
/**
Sets up the watcher, assuming one does not exist.
The target directory must exist.
*/
private func setupWatcher() {
// Guard against double setup
assert(watcher == nil, "setupWatcher() called when watcher already exists")
// Ensure that the target directory exists
if !App.shared.container.filesystem.anyExists(url.path) {
Log.warn("No watcher was created for \(url.path) because the requested directory does not exist.")
return
}
// Create a new FSNotifier which will respond to all events.
// If files are created, removed, etc. in this `homebrew/bin` folder, the handler will fire.
self.watcher = FSNotifier(for: url, eventMask: .all) { [weak self] in
guard let self = self else { return }
Task {
await self.onHomebrewPhpModification()
}
}
Log.perf("A watcher exists for Homebrew binaries at: \(url.relativePath)")
}
/**
Reloads PHP versions and refreshes the active PHP installation if any changes
are made to Homebrew binaries. Usually external changes to packages will trigger this.
As such, PHP Monitor will check if anything has changed with PHP.
*/
private func onHomebrewPhpModification() async {
await debouncer.debounce(for: debounceInterval) { [weak self] in
guard let self = self else { return }
Log.info("No changes in `\(self.url.path)` occurred for \(self.debounceInterval) seconds. Reloading now.")
// We reload the PHP versions in the background
await App.shared.container.phpEnvs.reloadPhpVersions()
// Finally, refresh the active installation
await MainMenu.shared.refreshActiveInstallation()
}
}
// MARK: - Suspend and resume
/**
Suspends the `HomebrewWatchManager`.
This prevents any changes to `/homebrew/bin` from causing events to fire.
*/
private func suspend() async {
await watcher?.suspend()
await debouncer.cancel()
}
/**
Resumes the `HomebrewWatchManager`.
Any changes to `/homebrew/bin` are picked up again.
*/
private func resume() async {
await watcher?.resume()
}
/**
Executes an `action` callback after suspending the watcher.
*/
private func withSuspended<T>(_ action: () async throws -> T) async rethrows -> T {
await suspend()
do {
let result = try await action()
await resume()
return result
} catch {
await resume()
throw error
}
}
}

View File

@@ -33,7 +33,7 @@ extension PhpVersionManagerView {
do { do {
self.setBusyStatus(true) self.setBusyStatus(true)
try await App.shared.withSuspendedHomebrewWatcher { try await HomebrewWatchManager.withSuspended {
try await command.execute(shell: container.shell) { progress in try await command.execute(shell: container.shell) { progress in
Task { @MainActor in Task { @MainActor in
self.status.title = progress.title self.status.title = progress.title
@@ -99,7 +99,7 @@ extension PhpVersionManagerView {
do { do {
self.setBusyStatus(true) self.setBusyStatus(true)
try await App.shared.withSuspendedHomebrewWatcher { try await HomebrewWatchManager.withSuspended {
try await command.execute(shell: container.shell) { progress in try await command.execute(shell: container.shell) { progress in
Task { @MainActor in Task { @MainActor in
self.status.title = progress.title self.status.title = progress.title

View File

@@ -41,10 +41,6 @@ struct FSNotifierTest {
} }
) )
defer {
notifier.terminate()
}
// Modify the file, twice // Modify the file, twice
try "hello".write(to: testFile, atomically: false, encoding: .utf8) try "hello".write(to: testFile, atomically: false, encoding: .utf8)
try "hello".write(to: testFile, atomically: false, encoding: .utf8) try "hello".write(to: testFile, atomically: false, encoding: .utf8)
@@ -59,5 +55,8 @@ struct FSNotifierTest {
// Verify after another second, our second write is actually noted // Verify after another second, our second write is actually noted
await delay(seconds: 1.2) await delay(seconds: 1.2)
#expect(eventFired.value == 2) #expect(eventFired.value == 2)
// Clean up notifier
notifier.terminate()
} }
} }