diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index 71748a5..e246095 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -7,6 +7,19 @@ objects = { /* Begin PBXBuildFile section */ + 0309E6672B0D4B2F002AC007 /* BrewExtensionsObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0309E6662B0D4B2F002AC007 /* BrewExtensionsObservable.swift */; }; + 033D45982B0D4EC600070080 /* InstallPhpExtensionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033D45972B0D4EC600070080 /* InstallPhpExtensionCommand.swift */; }; + 033D45992B0D4EC600070080 /* InstallPhpExtensionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033D45972B0D4EC600070080 /* InstallPhpExtensionCommand.swift */; }; + 033D459A2B0D4EC600070080 /* InstallPhpExtensionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033D45972B0D4EC600070080 /* InstallPhpExtensionCommand.swift */; }; + 033D459B2B0D4EC600070080 /* InstallPhpExtensionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033D45972B0D4EC600070080 /* InstallPhpExtensionCommand.swift */; }; + 033D459E2B0D513900070080 /* RemovePhpExtensionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033D459D2B0D513900070080 /* RemovePhpExtensionCommand.swift */; }; + 033D459F2B0D513900070080 /* RemovePhpExtensionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033D459D2B0D513900070080 /* RemovePhpExtensionCommand.swift */; }; + 033D45A02B0D513900070080 /* RemovePhpExtensionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033D459D2B0D513900070080 /* RemovePhpExtensionCommand.swift */; }; + 033D45A12B0D513900070080 /* RemovePhpExtensionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033D459D2B0D513900070080 /* RemovePhpExtensionCommand.swift */; }; + 033D45A32B0D531D00070080 /* PhpExtensionManagerView+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033D45A22B0D531D00070080 /* PhpExtensionManagerView+Actions.swift */; }; + 033D45A42B0D531D00070080 /* PhpExtensionManagerView+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033D45A22B0D531D00070080 /* PhpExtensionManagerView+Actions.swift */; }; + 033D45A52B0D531D00070080 /* PhpExtensionManagerView+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033D45A22B0D531D00070080 /* PhpExtensionManagerView+Actions.swift */; }; + 033D45A62B0D531D00070080 /* PhpExtensionManagerView+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033D45A22B0D531D00070080 /* PhpExtensionManagerView+Actions.swift */; }; 03E36FE728D9219000636F7F /* ActiveShell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E36FE628D9219000636F7F /* ActiveShell.swift */; }; 03E36FE828D9219000636F7F /* ActiveShell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E36FE628D9219000636F7F /* ActiveShell.swift */; }; 5420395926135DC100FB00FA /* PreferencesVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395826135DC100FB00FA /* PreferencesVC.swift */; }; @@ -159,10 +172,10 @@ C43A8A1A25D9CD1000591B77 /* Utility.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43A8A1925D9CD1000591B77 /* Utility.swift */; }; C43A8A2025D9D1D700591B77 /* brew-formula.json in Resources */ = {isa = PBXBuildFile; fileRef = C43A8A1F25D9D1D700591B77 /* brew-formula.json */; }; C43A8A2425D9D20D00591B77 /* HomebrewPackageTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43A8A2325D9D20D00591B77 /* HomebrewPackageTest.swift */; }; - C43BCD4429FBEF40001547BC /* InstallAndUpgradeCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43BCD4329FBEF40001547BC /* InstallAndUpgradeCommand.swift */; }; - C43BCD4529FBEF40001547BC /* InstallAndUpgradeCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43BCD4329FBEF40001547BC /* InstallAndUpgradeCommand.swift */; }; - C43BCD4629FBEF40001547BC /* InstallAndUpgradeCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43BCD4329FBEF40001547BC /* InstallAndUpgradeCommand.swift */; }; - C43BCD4729FBEF40001547BC /* InstallAndUpgradeCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43BCD4329FBEF40001547BC /* InstallAndUpgradeCommand.swift */; }; + C43BCD4429FBEF40001547BC /* ModifyPhpVersionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43BCD4329FBEF40001547BC /* ModifyPhpVersionCommand.swift */; }; + C43BCD4529FBEF40001547BC /* ModifyPhpVersionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43BCD4329FBEF40001547BC /* ModifyPhpVersionCommand.swift */; }; + C43BCD4629FBEF40001547BC /* ModifyPhpVersionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43BCD4329FBEF40001547BC /* ModifyPhpVersionCommand.swift */; }; + C43BCD4729FBEF40001547BC /* ModifyPhpVersionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43BCD4329FBEF40001547BC /* ModifyPhpVersionCommand.swift */; }; C43FDBE929A932B0003D85EC /* PhpConfigChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43FDBE829A932B0003D85EC /* PhpConfigChecker.swift */; }; C44067F527E2582B0045BD4E /* DomainListNameCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44067F427E2582B0045BD4E /* DomainListNameCell.swift */; }; C44067F727E258410045BD4E /* DomainListPhpCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44067F627E258410045BD4E /* DomainListPhpCell.swift */; }; @@ -871,7 +884,11 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 0309E6662B0D4B2F002AC007 /* BrewExtensionsObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewExtensionsObservable.swift; sourceTree = ""; }; 0336CAAF2B0D0CDA009A1034 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; + 033D45972B0D4EC600070080 /* InstallPhpExtensionCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallPhpExtensionCommand.swift; sourceTree = ""; }; + 033D459D2B0D513900070080 /* RemovePhpExtensionCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemovePhpExtensionCommand.swift; sourceTree = ""; }; + 033D45A22B0D531D00070080 /* PhpExtensionManagerView+Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PhpExtensionManagerView+Actions.swift"; sourceTree = ""; }; 03E36FE628D9219000636F7F /* ActiveShell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveShell.swift; sourceTree = ""; }; 5420395826135DC100FB00FA /* PreferencesVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesVC.swift; sourceTree = ""; }; 5420395E2613607600FB00FA /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; }; @@ -964,7 +981,7 @@ C43A8A1925D9CD1000591B77 /* Utility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utility.swift; sourceTree = ""; }; C43A8A1F25D9D1D700591B77 /* brew-formula.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "brew-formula.json"; sourceTree = ""; }; C43A8A2325D9D20D00591B77 /* HomebrewPackageTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewPackageTest.swift; sourceTree = ""; }; - C43BCD4329FBEF40001547BC /* InstallAndUpgradeCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallAndUpgradeCommand.swift; sourceTree = ""; }; + C43BCD4329FBEF40001547BC /* ModifyPhpVersionCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModifyPhpVersionCommand.swift; sourceTree = ""; }; C43FDBE829A932B0003D85EC /* PhpConfigChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpConfigChecker.swift; sourceTree = ""; }; C44067F427E2582B0045BD4E /* DomainListNameCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainListNameCell.swift; sourceTree = ""; }; C44067F627E258410045BD4E /* DomainListPhpCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainListPhpCell.swift; sourceTree = ""; }; @@ -1185,6 +1202,21 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 0309E6652B0D4B1D002AC007 /* Data */ = { + isa = PBXGroup; + children = ( + 0309E6662B0D4B2F002AC007 /* BrewExtensionsObservable.swift */, + ); + path = Data; + sourceTree = ""; + }; + 033D459C2B0D506B00070080 /* PHP Versions */ = { + isa = PBXGroup; + children = ( + ); + path = "PHP Versions"; + sourceTree = ""; + }; 5420395726135DB800FB00FA /* Preferences */ = { isa = PBXGroup; children = ( @@ -1430,6 +1462,7 @@ C4292D512B023F37004F0D2A /* PHP Extension Manager */ = { isa = PBXGroup; children = ( + 0309E6652B0D4B1D002AC007 /* Data */, C4292D522B023F52004F0D2A /* UI */, ); path = "PHP Extension Manager"; @@ -1440,6 +1473,7 @@ children = ( C4292D532B023F61004F0D2A /* PhpExtensionManagerWindowController.swift */, C4292D552B024006004F0D2A /* PhpExtensionManagerView.swift */, + 033D45A22B0D531D00070080 /* PhpExtensionManagerView+Actions.swift */, ); path = UI; sourceTree = ""; @@ -1927,9 +1961,12 @@ C4B79EBA29CA38D100A483EE /* Commands */ = { isa = PBXGroup; children = ( + 033D459C2B0D506B00070080 /* PHP Versions */, C4B79EBB29CA38DB00A483EE /* BrewCommand.swift */, - C43BCD4329FBEF40001547BC /* InstallAndUpgradeCommand.swift */, + C43BCD4329FBEF40001547BC /* ModifyPhpVersionCommand.swift */, C4B79ECA29CA475900A483EE /* RemovePhpVersionCommand.swift */, + 033D45972B0D4EC600070080 /* InstallPhpExtensionCommand.swift */, + 033D459D2B0D513900070080 /* RemovePhpExtensionCommand.swift */, ); path = Commands; sourceTree = ""; @@ -2472,6 +2509,7 @@ C45B91532956123A00F4EC78 /* FakeServicesManager.swift in Sources */, C41C708D28AA7F7900E8D498 /* NoWarningsView.swift in Sources */, C4080FF627BD8C6400BF2C6B /* BetterAlert.swift in Sources */, + 0309E6672B0D4B2F002AC007 /* BrewExtensionsObservable.swift in Sources */, C4E0F7ED27BEBDA9007475F2 /* NSWindowExtension.swift in Sources */, C4205A7E27F4D21800191A39 /* ValetProxy.swift in Sources */, C4C8E818276F54D8003AC782 /* App+ConfigWatch.swift in Sources */, @@ -2543,6 +2581,7 @@ 5420395F2613607600FB00FA /* Preferences.swift in Sources */, C48D0C9325CC804200CC7490 /* XibLoadable.swift in Sources */, 54FCFD2A276C8AA4004CE748 /* CheckboxPreferenceView.swift in Sources */, + 033D45982B0D4EC600070080 /* InstallPhpExtensionCommand.swift in Sources */, C47699F128A2F3150060FEB8 /* Warning.swift in Sources */, 54D9E0B227E4F51E003B9AD9 /* HotKeysController.swift in Sources */, C4811D2A22D70F9A00B5F6B3 /* MainMenu.swift in Sources */, @@ -2578,6 +2617,7 @@ C4B5853E2770FE3900DA4FBE /* Paths.swift in Sources */, C41C1B4B22B019FF00E7CF16 /* ActivePhpInstallation.swift in Sources */, C4FE011128084FC200D1DE6D /* SelectionVC.swift in Sources */, + 033D459E2B0D513900070080 /* RemovePhpExtensionCommand.swift in Sources */, C4709CA228524B3400088BB8 /* StatsView.swift in Sources */, C44CCD4027AFE2FC00CE40E5 /* AlertableError.swift in Sources */, C4B6091D2853AB9700C95265 /* ServicesView.swift in Sources */, @@ -2590,6 +2630,7 @@ C40C7F1E2772136000DDDCDC /* PhpEnvironments.swift in Sources */, C4B79EB629CA387F00A483EE /* BrewPhpFormulaeHandler.swift in Sources */, C476FF9822B0DD830098105B /* Alert.swift in Sources */, + 033D45A32B0D531D00070080 /* PhpExtensionManagerView+Actions.swift in Sources */, C474B00624C0E98C00066A22 /* LocalNotification.swift in Sources */, C4D5CFCA27E0F9CD00035329 /* NginxConfigurationFile.swift in Sources */, C4D36615291160A1006BD146 /* WIP.swift in Sources */, @@ -2616,7 +2657,7 @@ C4B79ECB29CA475900A483EE /* RemovePhpVersionCommand.swift in Sources */, C40D725F2A018AE30054A067 /* BrewFormula+UI.swift in Sources */, C4D89BC62783C99400A02B68 /* ComposerJson.swift in Sources */, - C43BCD4429FBEF40001547BC /* InstallAndUpgradeCommand.swift in Sources */, + C43BCD4429FBEF40001547BC /* ModifyPhpVersionCommand.swift in Sources */, C4E2E84A28FC1E70003B070C /* DataExtension.swift in Sources */, C46FA23F246C358E00944F05 /* StringExtension.swift in Sources */, C42337A3281F19F000459A48 /* Xdebug.swift in Sources */, @@ -2654,6 +2695,7 @@ C471E83928F9BB650021E251 /* ValetSite.swift in Sources */, C471E83A28F9BB650021E251 /* FakeValetSite.swift in Sources */, C471E83C28F9BB650021E251 /* ValetDomainScanner.swift in Sources */, + 033D459A2B0D4EC600070080 /* InstallPhpExtensionCommand.swift in Sources */, C4E2E86928FC3002003B070C /* Utility.swift in Sources */, C471E83D28F9BB650021E251 /* FakeDomainScanner.swift in Sources */, C471E83F28F9BB650021E251 /* AppDelegate.swift in Sources */, @@ -2663,6 +2705,7 @@ C471E84228F9BB650021E251 /* AppDelegate+InterApp.swift in Sources */, C471E84328F9BB650021E251 /* App.swift in Sources */, C4E2E85E28FC282B003B070C /* TestableConfiguration.swift in Sources */, + 033D45A52B0D531D00070080 /* PhpExtensionManagerView+Actions.swift in Sources */, C45E2A7529199248005C7CFD /* InternalSwitcherTest.swift in Sources */, C471E84428F9BB650021E251 /* App+ActivationPolicy.swift in Sources */, C471E84528F9BB650021E251 /* App+GlobalHotkey.swift in Sources */, @@ -2721,6 +2764,7 @@ C471E86B28F9BB650021E251 /* PreferenceName.swift in Sources */, C471E86C28F9BB650021E251 /* Preferences.swift in Sources */, C4D3660D29113F20006BD146 /* System.swift in Sources */, + 033D45A02B0D513900070080 /* RemovePhpExtensionCommand.swift in Sources */, C471E86D28F9BB650021E251 /* CustomPrefs.swift in Sources */, C45D654E29F52F74004C28F9 /* BrewPermissionFixer.swift in Sources */, C4E2E84C28FC1E70003B070C /* DataExtension.swift in Sources */, @@ -2798,7 +2842,7 @@ C45B914B295607F400F4EC78 /* Service.swift in Sources */, C471E7D928F9BA8F0021E251 /* TestableShell.swift in Sources */, C471E81428F9BAE80021E251 /* NSWindowExtension.swift in Sources */, - C43BCD4629FBEF40001547BC /* InstallAndUpgradeCommand.swift in Sources */, + C43BCD4629FBEF40001547BC /* ModifyPhpVersionCommand.swift in Sources */, C471E7D328F9BA8F0021E251 /* ActiveShell.swift in Sources */, C42106682AFA9FF400DF3732 /* PhpVersionManagerView+Actions.swift in Sources */, C4B79EC829CA474200A483EE /* FakeCommand.swift in Sources */, @@ -2840,6 +2884,7 @@ C471E89128F9BB8F0021E251 /* Errors.swift in Sources */, C4B79EC929CA474200A483EE /* FakeCommand.swift in Sources */, C471E89228F9BB8F0021E251 /* Alert.swift in Sources */, + 033D45A12B0D513900070080 /* RemovePhpExtensionCommand.swift in Sources */, C471E89328F9BB8F0021E251 /* Application.swift in Sources */, C471E89428F9BB8F0021E251 /* LocalNotification.swift in Sources */, C441CC592AE8249400DDFACD /* ConfigFSNotifier.swift in Sources */, @@ -2994,9 +3039,10 @@ C471E82C28F9BB340021E251 /* ValetListable.swift in Sources */, C471E82828F9BB310021E251 /* BrewDiagnostics.swift in Sources */, C471E81E28F9BB260021E251 /* BetterAlert.swift in Sources */, - C43BCD4729FBEF40001547BC /* InstallAndUpgradeCommand.swift in Sources */, + C43BCD4729FBEF40001547BC /* ModifyPhpVersionCommand.swift in Sources */, C44E985F29B23EBF0059F773 /* UpdateCheckTest.swift in Sources */, C471E7D228F9BA630021E251 /* ActiveFileSystem.swift in Sources */, + 033D45A62B0D531D00070080 /* PhpExtensionManagerView+Actions.swift in Sources */, C471E80028F9BAD10021E251 /* Xdebug.swift in Sources */, C471E7F528F9BAC80021E251 /* PhpEnvironments.swift in Sources */, C471E7ED28F9BAC30021E251 /* Process.swift in Sources */, @@ -3007,6 +3053,7 @@ C471E7CA28F9BA480021E251 /* TestableFileSystem.swift in Sources */, C471E7DD28F9BAA30021E251 /* CommandProtocol.swift in Sources */, C471E7D128F9BA630021E251 /* RealFileSystem.swift in Sources */, + 033D459B2B0D4EC600070080 /* InstallPhpExtensionCommand.swift in Sources */, C471E81D28F9BB260021E251 /* BetterAlertVC.swift in Sources */, C471E82B28F9BB340021E251 /* Valet.swift in Sources */, C471E80328F9BAD40021E251 /* PhpConfigurationFile.swift in Sources */, @@ -3110,6 +3157,7 @@ C456A0C72AA614BD0080144F /* PhpPreference.swift in Sources */, C42106672AFA9FF400DF3732 /* PhpVersionManagerView+Actions.swift in Sources */, C4C8E819276F54D8003AC782 /* App+ConfigWatch.swift in Sources */, + 033D45A42B0D531D00070080 /* PhpExtensionManagerView+Actions.swift in Sources */, C4FC21B128391F8E00D368BB /* MainMenu+Actions.swift in Sources */, 54D9E0B927E4F51E003B9AD9 /* KeyCombo.swift in Sources */, C4EED88A27A48778006D7272 /* InterAppHandler.swift in Sources */, @@ -3189,6 +3237,7 @@ C44C198E276E3A1C0072762D /* TerminalProgressWindowController.swift in Sources */, C4B79EBD29CA38DB00A483EE /* BrewCommand.swift in Sources */, C485707828BF456300539B36 /* Warning.swift in Sources */, + 033D459F2B0D513900070080 /* RemovePhpExtensionCommand.swift in Sources */, C415938027A1B54F00D2E1B7 /* ProjectTypeDetection.swift in Sources */, C40F505628ECA64E004AD45B /* TestableConfigurations.swift in Sources */, C4D9ADC9277611A0007277F4 /* InternalSwitcher.swift in Sources */, @@ -3198,6 +3247,7 @@ C471E79428F9B23B0021E251 /* FileSystemProtocol.swift in Sources */, C4F30B0B278E203C00755FCE /* MainMenu+Startup.swift in Sources */, C485707C28BF459500539B36 /* NoWarningsView.swift in Sources */, + 033D45992B0D4EC600070080 /* InstallPhpExtensionCommand.swift in Sources */, C4F5FBCD28218CB8001065C5 /* Xdebug.swift in Sources */, C40B24F227A310770018C7D2 /* Events.swift in Sources */, C490E3B829BCA367006D2DE6 /* App+BrewWatch.swift in Sources */, @@ -3237,7 +3287,7 @@ C4D36602291132B7006BD146 /* ValetScanners.swift in Sources */, C40934AB298EEDA900D25014 /* CaskFileParserTest.swift in Sources */, C436B39E29F3C42500B6A64E /* PreferencesTabs.swift in Sources */, - C43BCD4529FBEF40001547BC /* InstallAndUpgradeCommand.swift in Sources */, + C43BCD4529FBEF40001547BC /* ModifyPhpVersionCommand.swift in Sources */, C4551657297AED18009B8466 /* ValetRcTest.swift in Sources */, C464ADAD275A7A3F003FCD53 /* DomainListWindowController.swift in Sources */, C40C7F1F2772136000DDDCDC /* PhpEnvironments.swift in Sources */, diff --git a/phpmon/Common/State/BusyStatus.swift b/phpmon/Common/State/BusyStatus.swift index e2960cf..551d362 100644 --- a/phpmon/Common/State/BusyStatus.swift +++ b/phpmon/Common/State/BusyStatus.swift @@ -18,4 +18,12 @@ class BusyStatus: ObservableObject { self.title = title self.description = description } + + public static func notBusy() -> BusyStatus { + return BusyStatus(busy: false, title: "", description: "") + } + + public static func busy() -> BusyStatus { + return BusyStatus(busy: false, title: "", description: "") + } } diff --git a/phpmon/Domain/Integrations/Homebrew/BrewTapFormulae.swift b/phpmon/Domain/Integrations/Homebrew/BrewTapFormulae.swift index 25ad48a..c1ff4e3 100644 --- a/phpmon/Domain/Integrations/Homebrew/BrewTapFormulae.swift +++ b/phpmon/Domain/Integrations/Homebrew/BrewTapFormulae.swift @@ -8,15 +8,45 @@ import Foundation +struct BrewPhpExtension: Hashable, Comparable { + let name: String + let phpVersion: String + let isInstalled: Bool + + var formulaName: String { + return "\(name)@\(phpVersion)" + } + + init(name: String, phpVersion: String) { + self.name = name + self.phpVersion = phpVersion + self.isInstalled = BrewPhpExtension.hasInstallationReceipt( + for: "\(name)@\(phpVersion)" + ) + } + + static func hasInstallationReceipt(for formulaName: String) -> Bool { + return FileSystem.fileExists("\(Paths.optPath)/\(formulaName)/INSTALL_RECEIPT.json") + } + + static func < (lhs: BrewPhpExtension, rhs: BrewPhpExtension) -> Bool { + return lhs.name < rhs.name + } + + static func == (lhs: BrewPhpExtension, rhs: BrewPhpExtension) -> Bool { + return lhs.name == rhs.name + } +} + class BrewTapFormulae { - public static func from(tap: String) -> [String: Set] { + public static func from(tap: String) -> [String: [BrewPhpExtension]] { let directory = "\(Paths.tapPath)/\(tap)/Formula" let files = try? FileSystem.getShallowContentsOfDirectory(directory) - var availableExtensions = [String: Set]() + var availableExtensions = [String: [BrewPhpExtension]]() - guard let files else { + guard let files = files else { return availableExtensions } @@ -27,15 +57,22 @@ class BrewTapFormulae { if let match = matches.first { if let phpExtensionRange = Range(match.range(at: 1), in: file), let versionRange = Range(match.range(at: 2), in: file) { - let phpExtension = String(file[phpExtensionRange]) + // Determine what the extension's name is + let phpExtensionName = String(file[phpExtensionRange]) + // Determine what PHP version this is for let phpVersion = String(file[versionRange]) - if var existingExtensions = availableExtensions[phpVersion] { - existingExtensions.insert(phpExtension) - availableExtensions[phpVersion] = existingExtensions - } else { - availableExtensions[phpVersion] = [phpExtension] - } + // Create a new BrewPhpExtension object, which will determine + // whether this extension is installed or not + let phpExtension = BrewPhpExtension( + name: phpExtensionName, + phpVersion: phpVersion + ) + + // Append the extension to the list + var extensions = availableExtensions[phpVersion, default: []] + extensions.append(phpExtension) + availableExtensions[phpVersion] = extensions.sorted() } } } diff --git a/phpmon/Domain/Integrations/Homebrew/Commands/BrewCommand.swift b/phpmon/Domain/Integrations/Homebrew/Commands/BrewCommand.swift index 2f67281..2fb1d67 100644 --- a/phpmon/Domain/Integrations/Homebrew/Commands/BrewCommand.swift +++ b/phpmon/Domain/Integrations/Homebrew/Commands/BrewCommand.swift @@ -10,6 +10,8 @@ import Foundation protocol BrewCommand { func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws + + func getCommandTitle() -> String } extension BrewCommand { @@ -31,6 +33,44 @@ extension BrewCommand { } return nil } + + internal func run(_ command: String, _ onProgress: @escaping (BrewCommandProgress) -> Void) async throws { + var loggedMessages: [String] = [] + + let (process, _) = try! await Shell.attach( + command, + didReceiveOutput: { text, _ in + if !text.isEmpty { + Log.perf(text) + loggedMessages.append(text) + } + + if let (number, text) = self.reportInstallationProgress(text) { + onProgress(.create(value: number, title: getCommandTitle(), description: text)) + } + }, + withTimeout: .minutes(15) + ) + + if process.terminationStatus <= 0 { + loggedMessages = [] + return + } else { + throw BrewCommandError(error: "The command failed to run correctly.", log: loggedMessages) + } + } + + internal func checkPhpTap(_ onProgress: @escaping (BrewCommandProgress) -> Void) async throws { + if !BrewDiagnostics.installedTaps.contains("shivammathur/php") { + let command = "brew tap shivammathur/php" + try await run(command, onProgress) + } + + if !BrewDiagnostics.installedTaps.contains("shivammathur/extensions") { + let command = "brew tap shivammathur/extensions" + try await run(command, onProgress) + } + } } struct BrewCommandProgress { diff --git a/phpmon/Domain/Integrations/Homebrew/Commands/InstallPhpExtensionCommand.swift b/phpmon/Domain/Integrations/Homebrew/Commands/InstallPhpExtensionCommand.swift new file mode 100644 index 0000000..d959719 --- /dev/null +++ b/phpmon/Domain/Integrations/Homebrew/Commands/InstallPhpExtensionCommand.swift @@ -0,0 +1,81 @@ +// +// InstallPhpExtensionCommand.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 21/11/2023. +// Copyright © 2023 Nico Verbruggen. All rights reserved. +// + +import Foundation + +class InstallPhpExtensionCommand: BrewCommand { + let installing: [BrewPhpExtension] + + func getExtensionNames() -> String { + return installing.map { $0.name }.joined(separator: ", ") + } + + func getCommandTitle() -> String { + return "phpman.steps.installing".localized(getExtensionNames()) + } + + public init(install extensions: [BrewPhpExtension]) { + self.installing = extensions + } + + func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws { + let progressTitle = "phpman.steps.wait".localized + + onProgress(.create( + value: 0.2, + title: progressTitle, + description: "phpman.steps.preparing".localized + )) + + // Make sure the tap is installed + try await self.checkPhpTap(onProgress) + + // Make sure that the extension(s) are installed + try await self.installPackages(onProgress) + + // Finally, complete all operations + await self.completedOperations(onProgress) + } + + private func installPackages(_ onProgress: @escaping (BrewCommandProgress) -> Void) async throws { + // If no installations are needed, early exit + if self.installing.isEmpty { + return + } + + let command = """ + export HOMEBREW_NO_INSTALL_UPGRADE=true; \ + export HOMEBREW_NO_INSTALL_CLEANUP=true; \ + \(Paths.brew) install \(self.installing.map { $0.formulaName }.joined(separator: " ")) --force + """ + + try await run(command, onProgress) + } + + private func completedOperations(_ onProgress: @escaping (BrewCommandProgress) -> Void) async { + // Reload and restart PHP versions + onProgress(.create(value: 0.95, title: self.getCommandTitle(), description: "phpman.steps.reloading".localized)) + + // Check which version of PHP are now installed + await PhpEnvironments.detectPhpVersions() + + // Keep track of the currently installed version + await MainMenu.shared.refreshActiveInstallation() + + // Also rebuild the content of the main menu + await MainMenu.shared.rebuild() + + // Let the UI know that the installation has been completed + onProgress(.create( + value: 1, + title: "phpman.steps.completed".localized, + description: "phpman.steps.success".localized + )) + } + +} diff --git a/phpmon/Domain/Integrations/Homebrew/Commands/InstallAndUpgradeCommand.swift b/phpmon/Domain/Integrations/Homebrew/Commands/ModifyPhpVersionCommand.swift similarity index 78% rename from phpmon/Domain/Integrations/Homebrew/Commands/InstallAndUpgradeCommand.swift rename to phpmon/Domain/Integrations/Homebrew/Commands/ModifyPhpVersionCommand.swift index 4446f74..6b5c870 100644 --- a/phpmon/Domain/Integrations/Homebrew/Commands/InstallAndUpgradeCommand.swift +++ b/phpmon/Domain/Integrations/Homebrew/Commands/ModifyPhpVersionCommand.swift @@ -8,13 +8,16 @@ import Foundation -class InstallAndUpgradeCommand: BrewCommand { - +class ModifyPhpVersionCommand: BrewCommand { let title: String let installing: [BrewPhpFormula] let upgrading: [BrewPhpFormula] let phpGuard: PhpGuard + func getCommandTitle() -> String { + return title + } + /** You can pass in which PHP versions need to be upgraded and which ones need to be installed. The process will be executed in two steps: first upgrades, then installations. @@ -58,18 +61,6 @@ class InstallAndUpgradeCommand: BrewCommand { await self.completedOperations(onProgress) } - private func checkPhpTap(_ onProgress: @escaping (BrewCommandProgress) -> Void) async throws { - if !BrewDiagnostics.installedTaps.contains("shivammathur/php") { - let command = "brew tap shivammathur/php" - try await run(command, onProgress) - } - - if !BrewDiagnostics.installedTaps.contains("shivammathur/extensions") { - let command = "brew tap shivammathur/extensions" - try await run(command, onProgress) - } - } - private func upgradePackages(_ onProgress: @escaping (BrewCommandProgress) -> Void) async throws { // If no upgrades are needed, early exit if self.upgrading.isEmpty { @@ -132,32 +123,6 @@ class InstallAndUpgradeCommand: BrewCommand { try await run(command, onProgress) } - private func run(_ command: String, _ onProgress: @escaping (BrewCommandProgress) -> Void) async throws { - var loggedMessages: [String] = [] - - let (process, _) = try! await Shell.attach( - command, - didReceiveOutput: { text, _ in - if !text.isEmpty { - Log.perf(text) - loggedMessages.append(text) - } - - if let (number, text) = self.reportInstallationProgress(text) { - onProgress(.create(value: number, title: self.title, description: text)) - } - }, - withTimeout: .minutes(15) - ) - - if process.terminationStatus <= 0 { - loggedMessages = [] - return - } else { - throw BrewCommandError(error: "The command failed to run correctly.", log: loggedMessages) - } - } - private func completedOperations(_ onProgress: @escaping (BrewCommandProgress) -> Void) async { // Reload and restart PHP versions onProgress(.create(value: 0.95, title: self.title, description: "Reloading PHP versions...")) @@ -183,5 +148,4 @@ class InstallAndUpgradeCommand: BrewCommand { description: "The installation has succeeded." )) } - } diff --git a/phpmon/Domain/Integrations/Homebrew/Commands/RemovePhpExtensionCommand.swift b/phpmon/Domain/Integrations/Homebrew/Commands/RemovePhpExtensionCommand.swift new file mode 100644 index 0000000..1cab51f --- /dev/null +++ b/phpmon/Domain/Integrations/Homebrew/Commands/RemovePhpExtensionCommand.swift @@ -0,0 +1,60 @@ +// +// RemovePhpExtensionCommand.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 21/11/2023. +// Copyright © 2023 Nico Verbruggen. All rights reserved. +// + +import Foundation + +class RemovePhpExtensionCommand: BrewCommand { + public let phpExtension: BrewPhpExtension + + public init(remove formula: BrewPhpExtension) { + self.phpExtension = formula + } + + func getCommandTitle() -> String { + return "phpman.steps.removing".localized(phpExtension.name) + } + + func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws { + onProgress(.create( + value: 0.2, + title: getCommandTitle(), + description: "phpman.steps.removing".localized("`\(phpExtension.name)`...") + )) + + let command = """ + export HOMEBREW_NO_INSTALL_UPGRADE=true; \ + export HOMEBREW_NO_INSTALL_CLEANUP=true; \ + \(Paths.brew) remove \(phpExtension.formulaName) --force --ignore-dependencies + """ + + var loggedMessages: [String] = [] + + let (process, _) = try! await Shell.attach( + command, + didReceiveOutput: { text, _ in + if !text.isEmpty { + Log.perf(text) + loggedMessages.append(text) + } + }, + withTimeout: .minutes(5) + ) + + if process.terminationStatus <= 0 { + onProgress(.create(value: 0.95, title: getCommandTitle(), description: "phpman.steps.reloading".localized)) + + await PhpEnvironments.detectPhpVersions() + + await MainMenu.shared.refreshActiveInstallation() + + onProgress(.create(value: 1, title: getCommandTitle(), description: "phpman.steps.success".localized)) + } else { + throw BrewCommandError(error: "phpman.steps.failure".localized, log: loggedMessages) + } + } +} diff --git a/phpmon/Domain/Integrations/Homebrew/Commands/RemovePhpVersionCommand.swift b/phpmon/Domain/Integrations/Homebrew/Commands/RemovePhpVersionCommand.swift index e1d3612..b184596 100644 --- a/phpmon/Domain/Integrations/Homebrew/Commands/RemovePhpVersionCommand.swift +++ b/phpmon/Domain/Integrations/Homebrew/Commands/RemovePhpVersionCommand.swift @@ -21,12 +21,14 @@ class RemovePhpVersionCommand: BrewCommand { self.phpGuard = PhpGuard() } - func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws { - let progressTitle = "Removing PHP \(version)..." + func getCommandTitle() -> String { + return "Removing PHP \(version)..." + } + func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws { onProgress(.create( value: 0.2, - title: progressTitle, + title: getCommandTitle(), description: "Please wait while Homebrew removes PHP \(version)..." )) @@ -56,7 +58,7 @@ class RemovePhpVersionCommand: BrewCommand { ) if process.terminationStatus <= 0 { - onProgress(.create(value: 0.95, title: progressTitle, description: "Reloading PHP versions...")) + onProgress(.create(value: 0.95, title: getCommandTitle(), description: "Reloading PHP versions...")) await PhpEnvironments.detectPhpVersions() @@ -66,7 +68,7 @@ class RemovePhpVersionCommand: BrewCommand { await MainMenu.shared.switchToPhpVersionAndWait(version, silently: true) } - onProgress(.create(value: 1, title: progressTitle, description: "The operation has succeeded.")) + onProgress(.create(value: 1, title: getCommandTitle(), description: "The operation has succeeded.")) } else { throw BrewCommandError(error: "The command failed to run correctly.", log: loggedMessages) } diff --git a/phpmon/Domain/Integrations/Homebrew/Fake/FakeCommand.swift b/phpmon/Domain/Integrations/Homebrew/Fake/FakeCommand.swift index ebf704a..ff54fad 100644 --- a/phpmon/Domain/Integrations/Homebrew/Fake/FakeCommand.swift +++ b/phpmon/Domain/Integrations/Homebrew/Fake/FakeCommand.swift @@ -9,6 +9,10 @@ import Foundation class FakeCommand: BrewCommand { + func getCommandTitle() -> String { + return "Hello" + } + let version: String init(version: String) { diff --git a/phpmon/Domain/Menu/MainMenu.swift b/phpmon/Domain/Menu/MainMenu.swift index c100ae3..753384c 100644 --- a/phpmon/Domain/Menu/MainMenu.swift +++ b/phpmon/Domain/Menu/MainMenu.swift @@ -214,7 +214,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate @objc func openPhpVersionManager() { PhpVersionManagerWindowController.show() } - + @objc func openPhpExtensionManager() { PhpExtensionManagerWindowController.show() } diff --git a/phpmon/Modules/PHP Extension Manager/Data/BrewExtensionsObservable.swift b/phpmon/Modules/PHP Extension Manager/Data/BrewExtensionsObservable.swift new file mode 100644 index 0000000..5086f17 --- /dev/null +++ b/phpmon/Modules/PHP Extension Manager/Data/BrewExtensionsObservable.swift @@ -0,0 +1,21 @@ +// +// BrewExtensionsObservable.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 21/11/2023. +// Copyright © 2023 Nico Verbruggen. All rights reserved. +// + +import Foundation + +class BrewExtensionsObservable: ObservableObject { + @Published var extensions: [BrewPhpExtension] = [] + + public func loadExtensionData(for version: String) { + let tapFormulae = BrewTapFormulae.from(tap: "shivammathur/homebrew-extensions") + + if let filteredTapFormulae = tapFormulae[version] { + self.extensions = filteredTapFormulae + } + } +} diff --git a/phpmon/Modules/PHP Extension Manager/UI/PhpExtensionManagerView+Actions.swift b/phpmon/Modules/PHP Extension Manager/UI/PhpExtensionManagerView+Actions.swift new file mode 100644 index 0000000..8c4cbad --- /dev/null +++ b/phpmon/Modules/PHP Extension Manager/UI/PhpExtensionManagerView+Actions.swift @@ -0,0 +1,66 @@ +// +// PhpExtensionManagerView+Actions.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 21/11/2023. +// Copyright © 2023 Nico Verbruggen. All rights reserved. +// + +import Foundation +import Cocoa + +extension PhpExtensionManagerView { + public func presentErrorAlert( + title: String, + description: String, + button: String, + style: NSAlert.Style = .critical + ) { + Alert.confirm( + onWindow: App.shared.phpExtensionManagerWindowController!.window!, + messageText: title, + informativeText: description, + buttonTitle: button, + secondButtonTitle: "", + style: style, + onFirstButtonPressed: {} + ) + } + + public func runCommand(_ command: BrewCommand) async { + if PhpEnvironments.shared.isBusy { + self.presentErrorAlert( + title: "phpman.action_prevented_busy.title".localized, + description: "phpman.action_prevented_busy.desc".localized, + button: "generic.ok".localized + ) + return + } + + do { + self.status.busy = true + try await command.execute { progress in + Task { @MainActor in + self.status.title = progress.title + self.status.description = progress.description + self.status.busy = progress.value != 1 + } + } + + // TODO: Reload extensions + self.manager.loadExtensionData(for: self.phpVersion) + self.status.busy = false + } catch let error { + let error = error as! BrewCommandError + let messages = error.log.suffix(2).joined(separator: "\n") + + self.status.busy = false + + self.presentErrorAlert( + title: "phpman.failures.install.title".localized, + description: "phpman.failures.install.desc".localized(messages), + button: "generic.ok".localized + ) + } + } +} diff --git a/phpmon/Modules/PHP Extension Manager/UI/PhpExtensionManagerView.swift b/phpmon/Modules/PHP Extension Manager/UI/PhpExtensionManagerView.swift index 462aee1..a3a1605 100644 --- a/phpmon/Modules/PHP Extension Manager/UI/PhpExtensionManagerView.swift +++ b/phpmon/Modules/PHP Extension Manager/UI/PhpExtensionManagerView.swift @@ -9,45 +9,25 @@ import Foundation import SwiftUI -class BrewExtensionsObservable: ObservableObject { - @Published var extensions: [BrewPhpExtension] = [] { - didSet { - print(self.extensions) - } - } - - public func loadExtensionData(for version: String) { - let tapFormulae = BrewTapFormulae.from(tap: "shivammathur/homebrew-extensions") - if let filteredTapFormulae = tapFormulae[version] { - self.extensions = filteredTapFormulae.sorted().map({ name in - return BrewPhpExtension(name: name, isInstalled: false) - }) - } - } -} - -// Temp model for UI purposes -struct BrewPhpExtension { - let name: String - let isInstalled: Bool -} - struct PhpExtensionManagerView: View { - init() { - self.searchText = "" - self.phpVersion = PhpEnvironments.shared.currentInstall!.version.short - self.manager.loadExtensionData(for: self.phpVersion) - } - @ObservedObject var manager = BrewExtensionsObservable() + @ObservedObject var status: BusyStatus @State var searchText: String @State var phpVersion: String { didSet { self.manager.loadExtensionData(for: self.phpVersion) - print(self.manager.extensions) } } + init() { + self.searchText = "" + self.status = BusyStatus.busy() + self.phpVersion = PhpEnvironments.shared.currentInstall!.version.short + self.manager.loadExtensionData(for: self.phpVersion) + self.status.busy = false + #warning("PHP extension manager does not react to PHP version changes!") + } + var filteredExtensions: [BrewPhpExtension] { guard !searchText.isEmpty else { return manager.extensions @@ -59,14 +39,20 @@ struct PhpExtensionManagerView: View { VStack { header.padding(20) - List(Array(self.filteredExtensions.enumerated()), id: \.1.name) { (_, pExtension) in - listContent(for: pExtension) - .padding(.vertical, 8) - .padding(.horizontal, 8) + BlockingOverlayView( + busy: self.status.busy, + title: self.status.title, + text: self.status.description + ) { + List(Array(self.filteredExtensions.enumerated()), id: \.1.name) { (_, pExtension) in + listContent(for: pExtension) + .padding(.vertical, 8) + .padding(.horizontal, 8) + } + .edgesIgnoringSafeArea(.top) + .listStyle(PlainListStyle()) + .searchable(text: $searchText) } - .edgesIgnoringSafeArea(.top) - .listStyle(PlainListStyle()) - .searchable(text: $searchText) }.frame(width: 600, height: 600) } @@ -107,11 +93,11 @@ struct PhpExtensionManagerView: View { HStack { if bExtension.isInstalled { Button("phpman.buttons.uninstall".localizedForSwiftUI, role: .destructive) { - + Task { await self.runCommand(RemovePhpExtensionCommand(remove: bExtension)) } } } else { Button("phpman.buttons.install".localizedForSwiftUI) { - + Task { await self.runCommand(InstallPhpExtensionCommand(install: [bExtension])) } } } } diff --git a/phpmon/Modules/PHP Version Manager/UI/PhpVersionManagerView+Actions.swift b/phpmon/Modules/PHP Version Manager/UI/PhpVersionManagerView+Actions.swift index e40d201..5bd7127 100644 --- a/phpmon/Modules/PHP Version Manager/UI/PhpVersionManagerView+Actions.swift +++ b/phpmon/Modules/PHP Version Manager/UI/PhpVersionManagerView+Actions.swift @@ -10,7 +10,7 @@ import Foundation import SwiftUI extension PhpVersionManagerView { - public func runCommand(_ command: InstallAndUpgradeCommand) async { + public func runCommand(_ command: ModifyPhpVersionCommand) async { if PhpEnvironments.shared.isBusy { self.presentErrorAlert( title: "phpman.action_prevented_busy.title".localized, @@ -54,7 +54,7 @@ extension PhpVersionManagerView { } public func repairAll() async { - await self.runCommand(InstallAndUpgradeCommand( + await self.runCommand(ModifyPhpVersionCommand( title: "phpman.operations.repairing".localized, upgrading: [], installing: [] @@ -62,7 +62,7 @@ extension PhpVersionManagerView { } public func upgradeAll(_ formulae: [BrewPhpFormula]) async { - await self.runCommand(InstallAndUpgradeCommand( + await self.runCommand(ModifyPhpVersionCommand( title: "phpman.operations.updating".localized, upgrading: formulae, installing: [] @@ -70,7 +70,7 @@ extension PhpVersionManagerView { } public func install(_ formula: BrewPhpFormula) async { - await self.runCommand(InstallAndUpgradeCommand( + await self.runCommand(ModifyPhpVersionCommand( title: "phpman.operations.installing".localized(formula.displayName), upgrading: [], installing: [formula] diff --git a/phpmon/en.lproj/Localizable.strings b/phpmon/en.lproj/Localizable.strings index 7ad5117..fb59ba4 100644 --- a/phpmon/en.lproj/Localizable.strings +++ b/phpmon/en.lproj/Localizable.strings @@ -121,6 +121,15 @@ "phpman.buttons.repair" = "Repair"; "phpman.version.prerelease" = "Pre-release"; +"phpman.steps.installing" = "Installing %@"; +"phpman.steps.removing" = "Removing %@"; +"phpman.steps.reloading" = "Reloading PHP versions..."; +"phpman.steps.preparing" = "PHP Monitor is preparing Homebrew..."; +"phpman.steps.wait" = "Please wait..."; +"phpman.steps.completed" = "Operation completed!"; +"phpman.steps.success" = "The operation has succeeded."; +"phpman.steps.failure" = "The command failed to run correctly."; + "phpman.title" = "PHP Version Manager"; "phpman.description" = "**PHP Version Manager** lets you install, upgrade and delete different PHP versions via Homebrew without needing to run the commands in the terminal yourself."; "phpman.disclaimer" = "Please note that installing or upgrading PHP versions may cause other Homebrew packages to be upgraded as well. Most installation steps usually take some time, so please be patient while Homebrew does its job.";