mirror of
https://github.com/nicoverbruggen/phpmon.git
synced 2025-11-07 05:10:06 +01:00
🏗 SwiftUI experiments
This commit is contained in:
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
33
phpmon/Domain/App/Services/FakeServicesManager.swift
Normal file
33
phpmon/Domain/App/Services/FakeServicesManager.swift
Normal 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
|
||||
}
|
||||
}
|
||||
59
phpmon/Domain/App/Services/ServiceWrapper.swift
Normal file
59
phpmon/Domain/App/Services/ServiceWrapper.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
57
phpmon/Domain/App/Services/ServicesManager.swift
Normal file
57
phpmon/Domain/App/Services/ServicesManager.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
70
phpmon/Domain/App/Services/ValetServicesManager.swift
Normal file
70
phpmon/Domain/App/Services/ValetServicesManager.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user