1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2025-11-07 05:10:06 +01:00

🏗 SwiftUI experiments

This commit is contained in:
2022-12-23 19:20:04 +01:00
parent 923f0237e8
commit 44c1ea7de4
13 changed files with 395 additions and 153 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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