From 44c1ea7de45610a93463ae1d362ac09b43fc8853 Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Fri, 23 Dec 2022 19:20:04 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=8F=97=20SwiftUI=20experiments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PHP Monitor.xcodeproj/project.pbxproj | 40 +++++- .../xcschemes/PHP Monitor.xcscheme | 2 +- phpmon/Common/Core/Actions.swift | 4 +- phpmon/Common/Core/Homebrew.swift | 21 ++- .../Common/PHP/Homebrew/HomebrewService.swift | 8 +- phpmon/Common/PHP/PHP Version/PhpEnv.swift | 2 +- .../App/Services/FakeServicesManager.swift | 33 +++++ .../Domain/App/Services/ServiceWrapper.swift | 59 +++++++++ .../Domain/App/Services/ServicesManager.swift | 57 ++++++++ .../App/Services/ValetServicesManager.swift | 70 ++++++++++ phpmon/Domain/App/ServicesManager.swift | 125 ------------------ phpmon/Domain/Menu/MainMenu+Startup.swift | 2 +- phpmon/Domain/SwiftUI/Menu/ServicesView.swift | 125 +++++++++++++++--- 13 files changed, 395 insertions(+), 153 deletions(-) create mode 100644 phpmon/Domain/App/Services/FakeServicesManager.swift create mode 100644 phpmon/Domain/App/Services/ServiceWrapper.swift create mode 100644 phpmon/Domain/App/Services/ServicesManager.swift create mode 100644 phpmon/Domain/App/Services/ValetServicesManager.swift delete mode 100644 phpmon/Domain/App/ServicesManager.swift diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index cc7f358..a948178 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -143,6 +143,18 @@ C4570C3B28FC355300D18420 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = C473319E2470923A009A0597 /* Localizable.strings */; }; C4570C3C28FC355400D18420 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = C473319E2470923A009A0597 /* Localizable.strings */; }; C459B4BD27F6093700E9B4B4 /* nginx-proxy.test in Resources */ = {isa = PBXBuildFile; fileRef = C459B4BC27F6093700E9B4B4 /* nginx-proxy.test */; }; + C45B9149295607F400F4EC78 /* ServiceWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45B9148295607F400F4EC78 /* ServiceWrapper.swift */; }; + C45B914A295607F400F4EC78 /* ServiceWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45B9148295607F400F4EC78 /* ServiceWrapper.swift */; }; + C45B914B295607F400F4EC78 /* ServiceWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45B9148295607F400F4EC78 /* ServiceWrapper.swift */; }; + C45B914C295607F400F4EC78 /* ServiceWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45B9148295607F400F4EC78 /* ServiceWrapper.swift */; }; + C45B914E295608E300F4EC78 /* ValetServicesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45B914D295608E300F4EC78 /* ValetServicesManager.swift */; }; + C45B914F295608E300F4EC78 /* ValetServicesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45B914D295608E300F4EC78 /* ValetServicesManager.swift */; }; + C45B9150295608E300F4EC78 /* ValetServicesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45B914D295608E300F4EC78 /* ValetServicesManager.swift */; }; + C45B9151295608E300F4EC78 /* ValetServicesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45B914D295608E300F4EC78 /* ValetServicesManager.swift */; }; + C45B91532956123A00F4EC78 /* FakeServicesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45B91522956123A00F4EC78 /* FakeServicesManager.swift */; }; + C45B91542956123A00F4EC78 /* FakeServicesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45B91522956123A00F4EC78 /* FakeServicesManager.swift */; }; + C45B91552956123A00F4EC78 /* FakeServicesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45B91522956123A00F4EC78 /* FakeServicesManager.swift */; }; + C45B91562956123A00F4EC78 /* FakeServicesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45B91522956123A00F4EC78 /* FakeServicesManager.swift */; }; C45E2A7529199248005C7CFD /* InternalSwitcherTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C471E7AF28F9B4940021E251 /* InternalSwitcherTest.swift */; }; C45E2A77291992DA005C7CFD /* FeatureTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45E2A76291992DA005C7CFD /* FeatureTestCase.swift */; }; C45E76142854A65300B4FE0C /* ServicesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45E76132854A65300B4FE0C /* ServicesManager.swift */; }; @@ -786,6 +798,9 @@ C44F868D2835BD8D005C353A /* phpmon-config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "phpmon-config.json"; sourceTree = ""; }; C450C8C528C919EC002A2B4B /* PreferenceName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferenceName.swift; sourceTree = ""; }; C459B4BC27F6093700E9B4B4 /* nginx-proxy.test */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "nginx-proxy.test"; sourceTree = ""; }; + C45B9148295607F400F4EC78 /* ServiceWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceWrapper.swift; sourceTree = ""; }; + C45B914D295608E300F4EC78 /* ValetServicesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetServicesManager.swift; sourceTree = ""; }; + C45B91522956123A00F4EC78 /* FakeServicesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeServicesManager.swift; sourceTree = ""; }; C45E2A76291992DA005C7CFD /* FeatureTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureTestCase.swift; sourceTree = ""; }; C45E76132854A65300B4FE0C /* ServicesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServicesManager.swift; sourceTree = ""; }; C463E37F284930EE00422731 /* PresetHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresetHelper.swift; sourceTree = ""; }; @@ -1275,6 +1290,17 @@ path = php; sourceTree = ""; }; + C45B9147295607E200F4EC78 /* Services */ = { + isa = PBXGroup; + children = ( + C45E76132854A65300B4FE0C /* ServicesManager.swift */, + C45B914D295608E300F4EC78 /* ValetServicesManager.swift */, + C45B9148295607F400F4EC78 /* ServiceWrapper.swift */, + C45B91522956123A00F4EC78 /* FakeServicesManager.swift */, + ); + path = Services; + sourceTree = ""; + }; C464ADAA275A7A25003FCD53 /* DomainList */ = { isa = PBXGroup; children = ( @@ -1423,6 +1449,7 @@ C4B13B1D25C4915000548C3A /* App */ = { isa = PBXGroup; children = ( + C45B9147295607E200F4EC78 /* Services */, C41C1B3C22B0098000E7CF16 /* Main.storyboard */, C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */, C4B97B74275CF08C003F3378 /* AppDelegate+MenuOutlets.swift */, @@ -1436,7 +1463,6 @@ C495F5AE28A42E080087F70A /* EnvironmentCheck.swift */, C46E206C28299B3800D909D6 /* AppUpdateChecker.swift */, C40FE736282ABA4F00A302C2 /* AppVersion.swift */, - C45E76132854A65300B4FE0C /* ServicesManager.swift */, C4A6957528D23EE300A14CF8 /* EnvironmentManager.swift */, ); path = App; @@ -1954,8 +1980,10 @@ 5420395926135DC100FB00FA /* PrefsVC.swift in Sources */, C4C8900328F0E28800CE5E97 /* FileSystemProtocol.swift in Sources */, C43603A0275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */, + C45B9149295607F400F4EC78 /* ServiceWrapper.swift in Sources */, 5489625828312FAD004F647A /* CreatedFromFile.swift in Sources */, C4068CA727B07A1300544CD5 /* SelectPreferenceView.swift in Sources */, + C45B91532956123A00F4EC78 /* FakeServicesManager.swift in Sources */, C41C708D28AA7F7900E8D498 /* NoWarningsView.swift in Sources */, C4080FF627BD8C6400BF2C6B /* BetterAlert.swift in Sources */, C4E0F7ED27BEBDA9007475F2 /* NSWindowExtension.swift in Sources */, @@ -1967,6 +1995,7 @@ C4E4404627C56F4700D225E1 /* ValetSite.swift in Sources */, C4F2E43A2752F7D00020E974 /* PhpInstallation.swift in Sources */, C4D9F24B280B69E100DCD39A /* AddProxyVC.swift in Sources */, + C45B914E295608E300F4EC78 /* ValetServicesManager.swift in Sources */, C4E49DED28F764A00026AC4E /* TestableCommand.swift in Sources */, C4A6957628D23EE300A14CF8 /* EnvironmentManager.swift in Sources */, C41E871A2763D42300161EE0 /* DomainListVC+ContextMenu.swift in Sources */, @@ -2155,6 +2184,7 @@ C471E86328F9BB650021E251 /* PMTableView.swift in Sources */, C471E86428F9BB650021E251 /* Warning.swift in Sources */, C40175BA2903108900763A68 /* ValetInteractor.swift in Sources */, + C45B9150295608E300F4EC78 /* ValetServicesManager.swift in Sources */, C471E86528F9BB650021E251 /* WarningManager.swift in Sources */, C471E86628F9BB650021E251 /* WarningsWindowController.swift in Sources */, C471E86728F9BB650021E251 /* OnboardingWindowController.swift in Sources */, @@ -2196,6 +2226,7 @@ C471E88E28F9BB650021E251 /* KeyCombo.swift in Sources */, C471E88F28F9BB650021E251 /* ModifierFlagsExtension.swift in Sources */, C471E7E928F9BAC20021E251 /* Paths.swift in Sources */, + C45B91552956123A00F4EC78 /* FakeServicesManager.swift in Sources */, C471E7FE28F9BACE0021E251 /* HomebrewPackage.swift in Sources */, C471E7D828F9BA8F0021E251 /* FileSystemProtocol.swift in Sources */, C471E7F328F9BAC70021E251 /* PhpHelper.swift in Sources */, @@ -2225,6 +2256,7 @@ C471E7F228F9BAC70021E251 /* PhpEnv.swift in Sources */, C471E7E628F9BAC20021E251 /* Process.swift in Sources */, C471E81928F9BAE80021E251 /* NSMenuItemExtension.swift in Sources */, + C45B914B295607F400F4EC78 /* ServiceWrapper.swift in Sources */, C471E7D928F9BA8F0021E251 /* TestableShell.swift in Sources */, C471E81428F9BAE80021E251 /* NSWindowExtension.swift in Sources */, C471E7D328F9BA8F0021E251 /* ActiveShell.swift in Sources */, @@ -2277,6 +2309,7 @@ C471E8A528F9BB8F0021E251 /* AppDelegate+InterApp.swift in Sources */, C471E8A628F9BB8F0021E251 /* App.swift in Sources */, C471E8A728F9BB8F0021E251 /* App+ActivationPolicy.swift in Sources */, + C45B914C295607F400F4EC78 /* ServiceWrapper.swift in Sources */, C471E8A828F9BB8F0021E251 /* App+GlobalHotkey.swift in Sources */, C471E8A928F9BB8F0021E251 /* InterAppHandler.swift in Sources */, C471E8AA28F9BB8F0021E251 /* Startup.swift in Sources */, @@ -2309,6 +2342,7 @@ C4D36618291160A1006BD146 /* WIP.swift in Sources */, C471E8C328F9BB8F0021E251 /* SelectionVC.swift in Sources */, C471E8C428F9BB8F0021E251 /* AddSiteVC.swift in Sources */, + C45B91562956123A00F4EC78 /* FakeServicesManager.swift in Sources */, C471E8C528F9BB8F0021E251 /* AddProxyVC.swift in Sources */, C471E8C628F9BB8F0021E251 /* PMTableView.swift in Sources */, C471E8C728F9BB8F0021E251 /* Warning.swift in Sources */, @@ -2347,6 +2381,7 @@ C471E8EA28F9BB8F0021E251 /* SectionHeaderView.swift in Sources */, C4D36604291132B7006BD146 /* ValetScanners.swift in Sources */, C471E8EB28F9BB8F0021E251 /* HeaderView.swift in Sources */, + C45B9151295608E300F4EC78 /* ValetServicesManager.swift in Sources */, C471E8EC28F9BB8F0021E251 /* SwiftUIHelper.swift in Sources */, C471E8EE28F9BB8F0021E251 /* HotKey.swift in Sources */, C471E8EF28F9BB8F0021E251 /* HotKeysController.swift in Sources */, @@ -2465,6 +2500,7 @@ C4AF9F7B2754499000D44ED0 /* Valet.swift in Sources */, C4C1019C27C65C6F001FACC2 /* Process.swift in Sources */, C4F780C025D80B6E000DBC97 /* Startup.swift in Sources */, + C45B914A295607F400F4EC78 /* ServiceWrapper.swift in Sources */, C4C0E8E327F88B13002D32A9 /* ValetDomainScanner.swift in Sources */, C4CCBA6D275C567B008C7055 /* PMWindowController.swift in Sources */, C4B5635F276AB09000F12CCB /* VersionExtractor.swift in Sources */, @@ -2530,6 +2566,7 @@ C4D3660C29113F20006BD146 /* System.swift in Sources */, C4D936CA27E3EB6100BD69FE /* PhpHelper.swift in Sources */, C4D36611291140BE006BD146 /* TestableFileSystemTest.swift in Sources */, + C45B91542956123A00F4EC78 /* FakeServicesManager.swift in Sources */, C4E2E84B28FC1E70003B070C /* DataExtension.swift in Sources */, C449B4F127EE7FC200C47E8A /* DomainListNameCell.swift in Sources */, C4F780BA25D80B62000DBC97 /* AppDelegate.swift in Sources */, @@ -2559,6 +2596,7 @@ C4B585452770FE3900DA4FBE /* RealCommand.swift in Sources */, C4F780C525D80B75000DBC97 /* MenuBarImageGenerator.swift in Sources */, C4F780B725D80B5D000DBC97 /* App.swift in Sources */, + C45B914F295608E300F4EC78 /* ValetServicesManager.swift in Sources */, C4927F0C27B2DFC200C55AFD /* Errors.swift in Sources */, C485707628BF455100539B36 /* SectionHeaderView.swift in Sources */, C46EBC4828DB9644007ACC74 /* RealShell.swift in Sources */, diff --git a/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor.xcscheme b/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor.xcscheme index b7409be..864a93d 100644 --- a/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor.xcscheme +++ b/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor.xcscheme @@ -109,7 +109,7 @@ + isEnabled = "YES"> diff --git a/phpmon/Common/Core/Actions.swift b/phpmon/Common/Core/Actions.swift index ed62f17..d77f9e7 100644 --- a/phpmon/Common/Core/Actions.swift +++ b/phpmon/Common/Core/Actions.swift @@ -69,7 +69,7 @@ class Actions { public static func stopService(name: String) async { await brew( "services stop \(name)", - sudo: ServicesManager.shared.services[name]?.formula.elevated ?? false + sudo: ServicesManager.shared[name]?.formula.elevated ?? false ) await ServicesManager.loadHomebrewServices() } @@ -77,7 +77,7 @@ class Actions { public static func startService(name: String) async { await brew( "services start \(name)", - sudo: ServicesManager.shared.services[name]?.formula.elevated ?? false + sudo: ServicesManager.shared[name]?.formula.elevated ?? false ) await ServicesManager.loadHomebrewServices() } diff --git a/phpmon/Common/Core/Homebrew.swift b/phpmon/Common/Core/Homebrew.swift index 02f5e0f..a8193e3 100644 --- a/phpmon/Common/Core/Homebrew.swift +++ b/phpmon/Common/Core/Homebrew.swift @@ -9,8 +9,18 @@ import Foundation class Homebrew { + static var fake: Bool = false + struct Formulae { static var php: HomebrewFormula { + if Homebrew.fake { + return HomebrewFormula("php", elevated: true) + } + + if PhpEnv.shared.homebrewPackage == nil { + fatalError("You must either load the HomebrewPackage object or call `fake` on the Homebrew class.") + } + return HomebrewFormula(PhpEnv.phpInstall.formula, elevated: true) } @@ -26,7 +36,7 @@ class Homebrew { } } -class HomebrewFormula { +class HomebrewFormula: Equatable, Hashable { let name: String let elevated: Bool @@ -34,4 +44,13 @@ class HomebrewFormula { self.name = name self.elevated = elevated } + + static func == (lhs: HomebrewFormula, rhs: HomebrewFormula) -> Bool { + return lhs.elevated == rhs.elevated && lhs.name == rhs.name + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(name) + hasher.combine(elevated) + } } diff --git a/phpmon/Common/PHP/Homebrew/HomebrewService.swift b/phpmon/Common/PHP/Homebrew/HomebrewService.swift index 3d7a361..648f781 100644 --- a/phpmon/Common/PHP/Homebrew/HomebrewService.swift +++ b/phpmon/Common/PHP/Homebrew/HomebrewService.swift @@ -8,7 +8,7 @@ import Foundation -struct HomebrewService: Decodable, Equatable { +struct HomebrewService: Decodable, Equatable, Hashable { let name: String let service_name: String let running: Bool @@ -35,4 +35,10 @@ struct HomebrewService: Decodable, Equatable { error_log_path: nil ) } + + public func hash(into hasher: inout Hasher) { + hasher.combine(name) + hasher.combine(service_name) + hasher.combine(pid) + } } diff --git a/phpmon/Common/PHP/PHP Version/PhpEnv.swift b/phpmon/Common/PHP/PHP Version/PhpEnv.swift index 8ec9a68..9d83559 100644 --- a/phpmon/Common/PHP/PHP Version/PhpEnv.swift +++ b/phpmon/Common/PHP/PHP Version/PhpEnv.swift @@ -45,7 +45,7 @@ class PhpEnv { var cachedPhpInstallations: [String: PhpInstallation] = [:] /** Information about the currently linked PHP installation. */ - var currentInstall: ActivePhpInstallation + var currentInstall: ActivePhpInstallation! /** The version that the `php` formula via Brew is aliased to on the current system. diff --git a/phpmon/Domain/App/Services/FakeServicesManager.swift b/phpmon/Domain/App/Services/FakeServicesManager.swift new file mode 100644 index 0000000..1008bd2 --- /dev/null +++ b/phpmon/Domain/App/Services/FakeServicesManager.swift @@ -0,0 +1,33 @@ +// +// FakeServicesManager.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 23/12/2022. +// Copyright © 2022 Nico Verbruggen. All rights reserved. +// + +import Foundation + +class FakeServicesManager: ServicesManager { + override init() { + Log.warn("A fake services manager is being used, so Homebrew formula resolver is set to act in fake mode.") + Log.warn("If you do not want this behaviour, never instantiate FakeServicesManager!") + Homebrew.fake = true + } + + override var formulae: [HomebrewFormula] { + var formulae = [ + Homebrew.Formulae.php, + Homebrew.Formulae.nginx, + Homebrew.Formulae.dnsmasq + ] + + let additionalFormulae = ["mailhog", "coolio"].map({ name in + return HomebrewFormula(name, elevated: false) + }) + + formulae.append(contentsOf: additionalFormulae) + + return formulae + } +} diff --git a/phpmon/Domain/App/Services/ServiceWrapper.swift b/phpmon/Domain/App/Services/ServiceWrapper.swift new file mode 100644 index 0000000..c9229ff --- /dev/null +++ b/phpmon/Domain/App/Services/ServiceWrapper.swift @@ -0,0 +1,59 @@ +// +// ServiceWrapper.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 23/12/2022. +// Copyright © 2022 Nico Verbruggen. All rights reserved. +// + +import Foundation + +/** + Whether a given service is active, inactive or PHP Monitor is still busy determining the status. + */ +public enum ServiceStatus: String { + case active + case inactive + case loading + case missing +} + +/** + Service wrapper, that contains the Homebrew JSON output (if determined) and the formula. + This helps the app determine whether a service should run as an administrator or not. + */ +public class ServiceWrapper: ObservableObject, Identifiable, Hashable { + var formula: HomebrewFormula + var service: HomebrewService? + + var isBusy: Bool = false + + public var name: String { + return formula.name + } + + public var status: ServiceStatus { + if isBusy { + return .loading + } + + guard let service = self.service else { + return .missing + } + + return service.running ? .active : .inactive + } + + init(formula: HomebrewFormula) { + self.formula = formula + } + + public static func == (lhs: ServiceWrapper, rhs: ServiceWrapper) -> Bool { + return lhs.service == rhs.service && lhs.formula == rhs.formula + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(formula) + hasher.combine(service) + } +} diff --git a/phpmon/Domain/App/Services/ServicesManager.swift b/phpmon/Domain/App/Services/ServicesManager.swift new file mode 100644 index 0000000..944195e --- /dev/null +++ b/phpmon/Domain/App/Services/ServicesManager.swift @@ -0,0 +1,57 @@ +// +// ServicesManager.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 11/06/2022. +// Copyright © 2022 Nico Verbruggen. All rights reserved. +// + +import Foundation +import SwiftUI + +class ServicesManager: ObservableObject { + + @ObservedObject static var shared: ServicesManager = ValetServicesManager() + + @Published private(set) var services = [ServiceWrapper]() + + subscript(name: String) -> ServiceWrapper? { + return self.services.first { wrapper in + wrapper.name == name + } + } + + @available(*, deprecated, message: "Use a more specific method instead") + static func loadHomebrewServices() { + print(self.shared) + print("This method must be updated") + } + + public func updateServices() { + fatalError("Must be implemented in child class") + } + + var formulae: [HomebrewFormula] { + var formulae = [ + Homebrew.Formulae.php, + Homebrew.Formulae.nginx, + Homebrew.Formulae.dnsmasq + ] + + let additionalFormulae = (Preferences.custom.services ?? []).map({ item in + return HomebrewFormula(item, elevated: false) + }) + + formulae.append(contentsOf: additionalFormulae) + + return formulae + } + + init() { + Log.info("The services manager will determine which Valet services exist on this system.") + + services = formulae.map { + ServiceWrapper(formula: $0) + } + } +} diff --git a/phpmon/Domain/App/Services/ValetServicesManager.swift b/phpmon/Domain/App/Services/ValetServicesManager.swift new file mode 100644 index 0000000..6319169 --- /dev/null +++ b/phpmon/Domain/App/Services/ValetServicesManager.swift @@ -0,0 +1,70 @@ +// +// ValetServicesManager.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 23/12/2022. +// Copyright © 2022 Nico Verbruggen. All rights reserved. +// + +import Foundation + +class ValetServicesManager: ServicesManager { + override init() { + super.init() + + // Load the initial services state + self.updateServices() + } + + override func updateServices() { + // TODO + } +} + +// TODO + +/* +public static func loadHomebrewServices() async { + await Self.shared.updateServicesList() + + Task { + let rootServiceNames = Self.shared.formulae + .filter { $0.elevated } + .map { $0.name } + + let rootJson = await Shell + .pipe("sudo \(Paths.brew) services info --all --json") + .out.data(using: .utf8)! + + let rootServices = try! JSONDecoder() + .decode([HomebrewService].self, from: rootJson) + .filter({ return rootServiceNames.contains($0.name) }) + + Task { @MainActor in + for service in rootServices { + Self.shared.services[service.name]!.service = service + } + } + } + + Task { + let userServiceNames = Self.shared.formulae + .filter { !$0.elevated } + .map { $0.name } + + let normalJson = await Shell + .pipe("\(Paths.brew) services info --all --json") + .out.data(using: .utf8)! + + let userServices = try! JSONDecoder() + .decode([HomebrewService].self, from: normalJson) + .filter({ return userServiceNames.contains($0.name) }) + + Task { @MainActor in + for service in userServices { + Self.shared.services[service.name]!.service = service + } + } + } +} +*/ diff --git a/phpmon/Domain/App/ServicesManager.swift b/phpmon/Domain/App/ServicesManager.swift deleted file mode 100644 index eab61ac..0000000 --- a/phpmon/Domain/App/ServicesManager.swift +++ /dev/null @@ -1,125 +0,0 @@ -// -// ServicesManager.swift -// PHP Monitor -// -// Created by Nico Verbruggen on 11/06/2022. -// Copyright © 2022 Nico Verbruggen. All rights reserved. -// - -import Foundation -import SwiftUI - -class ServicesManager: ObservableObject { - - static var shared = ServicesManager() - - @Published private(set) var formulae: [HomebrewFormula] - - @Published private(set) var services: [String: ServiceWrapper] = [:] - - init() { - Log.info("Initializing ServicesManager...") - - formulae = [ - Homebrew.Formulae.php, - Homebrew.Formulae.nginx, - Homebrew.Formulae.dnsmasq - ] - - let additionalFormulae = (Preferences.custom.services ?? []).map({ item in - return HomebrewFormula(item, elevated: false) - }) - - formulae.append(contentsOf: additionalFormulae) - - services = Dictionary(uniqueKeysWithValues: formulae.map { ($0.name, ServiceWrapper(formula: $0)) }) - } - - public func updateServicesList() async { - Task { @MainActor in - formulae = [ - Homebrew.Formulae.php, - Homebrew.Formulae.nginx, - Homebrew.Formulae.dnsmasq - ] - - let additionalFormulae = (Preferences.custom.services ?? []).map({ item in - return HomebrewFormula(item, elevated: false) - }) - - formulae.append(contentsOf: additionalFormulae) - - services = Dictionary(uniqueKeysWithValues: formulae.map { ($0.name, ServiceWrapper(formula: $0)) }) - } - } - - public static func loadHomebrewServices() async { - await Self.shared.updateServicesList() - - Task { - let rootServiceNames = Self.shared.formulae - .filter { $0.elevated } - .map { $0.name } - - let rootJson = await Shell - .pipe("sudo \(Paths.brew) services info --all --json") - .out.data(using: .utf8)! - - let rootServices = try! JSONDecoder() - .decode([HomebrewService].self, from: rootJson) - .filter({ return rootServiceNames.contains($0.name) }) - - Task { @MainActor in - for service in rootServices { - Self.shared.services[service.name]!.service = service - } - } - } - - Task { - let userServiceNames = Self.shared.formulae - .filter { !$0.elevated } - .map { $0.name } - - let normalJson = await Shell - .pipe("\(Paths.brew) services info --all --json") - .out.data(using: .utf8)! - - let userServices = try! JSONDecoder() - .decode([HomebrewService].self, from: normalJson) - .filter({ return userServiceNames.contains($0.name) }) - - Task { @MainActor in - for service in userServices { - Self.shared.services[service.name]!.service = service - } - } - } - } - - /** - Service wrapper, that contains the Homebrew JSON output (if determined) and the formula. - This helps the app determine whether a service should run as an administrator or not. - */ - public struct ServiceWrapper { - public var formula: HomebrewFormula - public var service: HomebrewService? - - init(formula: HomebrewFormula) { - self.formula = formula - } - } - - /** - Dummy data for preview purposes. - */ - func withDummyServices(_ services: [String: Bool]) -> Self { - for (service, enabled) in services { - var item = ServiceWrapper(formula: HomebrewFormula(service)) - item.service = HomebrewService.dummy(named: service, enabled: enabled) - self.services[service] = item - } - - return self - } -} diff --git a/phpmon/Domain/Menu/MainMenu+Startup.swift b/phpmon/Domain/Menu/MainMenu+Startup.swift index 3896868..2698049 100644 --- a/phpmon/Domain/Menu/MainMenu+Startup.swift +++ b/phpmon/Domain/Menu/MainMenu+Startup.swift @@ -96,7 +96,7 @@ extension MainMenu { Valet.notifyAboutUnsupportedTLD() // Find out which services are active - await ServicesManager.loadHomebrewServices() + Log.info("The services manager knows about \(ServicesManager.shared.services.count) services.") // Start the background refresh timer startSharedTimer() diff --git a/phpmon/Domain/SwiftUI/Menu/ServicesView.swift b/phpmon/Domain/SwiftUI/Menu/ServicesView.swift index b142ba8..cc2f0a7 100644 --- a/phpmon/Domain/SwiftUI/Menu/ServicesView.swift +++ b/phpmon/Domain/SwiftUI/Menu/ServicesView.swift @@ -10,44 +10,129 @@ import Foundation import SwiftUI struct ServicesView: View { - static func asMenuItem(perRow: Int = 3) -> NSMenuItem { + static func asMenuItem(perRow: Int = 4) -> NSMenuItem { let item = NSMenuItem() - let view = NSHostingView( - rootView: Self() + let manager = ServicesManager.shared + + let rootView = Self( + manager: manager, + perRow: perRow ) + let view = NSHostingView(rootView: rootView) view.autoresizingMask = [.width, .height] + view.setFrameSize( + CGSize(width: view.frame.width, height: rootView.height) + ) + view.focusRingType = .none - let height = CGFloat(45 * ["a", "b", "c", "d", "e", "f"] - .chunked(by: perRow).count) - - view.setFrameSize(CGSize(width: view.frame.width, height: height)) item.view = view return item } + @ObservedObject var manager: ServicesManager + var perRow: Int + var height: CGFloat + var chunkCount: Int + + init(manager: ServicesManager, perRow: Int, height: CGFloat? = nil) { + self.manager = manager + self.perRow = perRow + self.chunkCount = manager.services.chunked(by: perRow).count + self.height = CGFloat((50 * chunkCount) + (5 * perRow)) + } + var body: some View { - Text("WIP") - .padding(10) - .frame(minWidth: 0, maxWidth: .infinity) + GeometryReader { geometry in + VStack { + ForEach(manager.services.chunked(by: perRow), id: \.self) { chunk in + HStack { + ForEach(chunk) { service in + ServiceView(service: service) + .frame(width: abs((geometry.size.width - 15) / CGFloat(perRow))) + } + } + } + } + .padding(.top, 10) + } + .frame(height: self.height) .background(Color.debug) } } +struct ServiceView: View { + @ObservedObject var service: ServiceWrapper + + var body: some View { + VStack(spacing: 0) { + Text(service.name.uppercased()) + .font(.system(size: 10)) + .frame(minWidth: 0, maxWidth: .infinity) + .padding(.bottom, 4) + .background(Color.debug) + if service.status == .loading { + ProgressView() + .scaleEffect(x: 0.5, y: 0.5, anchor: .center) + .frame(width: 16.0, height: 20.0) + } + if service.status == .missing { + Button { print("we pressed da button ")} label: { + Text("?") + } + .buttonStyle(BlueButton()) + } + if service.status == .active { + Button { + // TODO + } label: { + Image(systemName: "checkmark") + .resizable() + .frame(width: 12.0, height: 12.0) + .foregroundColor(Color("IconColorGreen")) + } + } + if service.status == .inactive { + Button { + // TODO + } label: { + Image(systemName: "xmark") + .resizable() + .frame(width: 12.0, height: 12.0) + .foregroundColor(Color("IconColorRed")) + } + } + } + } +} + +public struct BlueButton: ButtonStyle { + public func makeBody(configuration: Configuration) -> some View { + configuration.label + .padding(.bottom, 5) + .padding(.top, 5) + .padding(.leading, 10) + .padding(.trailing, 10) + .background(Color(red: 0, green: 0, blue: 0.5)) + .foregroundColor(.white) + .clipShape(Capsule()) + } +} + struct ServicesView_Previews: PreviewProvider { static var previews: some View { - ServicesView() - .frame(width: 330.0) - .previewDisplayName("Loading") + ServicesView(manager: FakeServicesManager(), perRow: 3) + .frame(width: 330.0) + .previewDisplayName("Loading") - ServicesView() - .frame(width: 330.0) - .previewDisplayName("Light Mode") + ServicesView(manager: FakeServicesManager(), perRow: 3) + .frame(width: 330.0) + .previewDisplayName("Light Mode") - ServicesView() - .frame(width: 330.0) - .previewDisplayName("Dark Mode") - .preferredColorScheme(.dark) + ServicesView(manager: FakeServicesManager(), perRow: 3) + .frame(width: 330.0) + .previewDisplayName("Dark Mode") + .preferredColorScheme(.dark) } }