PadXcode-Daemon/Preferences/AppPreferences.swift
2026-04-12 00:42:51 -07:00

83 lines
2.8 KiB
Swift

import Foundation
import ServiceManagement
import AppKit
final class AppPreferences: ObservableObject {
@Published var port: Int {
didSet { UserDefaults.standard.set(port, forKey: "daemonPort") }
}
@Published var showInDock: Bool {
didSet {
UserDefaults.standard.set(showInDock, forKey: "showInDock")
applyDockPolicy()
}
}
@Published var developmentTeam: String {
didSet { UserDefaults.standard.set(developmentTeam, forKey: "developmentTeam") }
}
@Published var allowProvisioningUpdates: Bool {
didSet { UserDefaults.standard.set(allowProvisioningUpdates, forKey: "allowProvisioningUpdates") }
}
@Published var buildConfiguration: String {
didSet { UserDefaults.standard.set(buildConfiguration, forKey: "buildConfiguration") }
}
var launchAtLogin: Bool {
get { SMAppService.mainApp.status == .enabled }
set {
do {
if newValue { try SMAppService.mainApp.register() }
else { try SMAppService.mainApp.unregister() }
objectWillChange.send()
} catch { print("SMAppService error: \(error)") }
}
}
var tailscaleIP: String {
TailscaleDetector.currentIP() ?? "Not detected"
}
init() {
let d = UserDefaults.standard
self.port = d.integer(forKey: "daemonPort").nonZero ?? 8080
self.showInDock = d.bool(forKey: "showInDock")
self.developmentTeam = d.string(forKey: "developmentTeam") ?? ""
self.allowProvisioningUpdates = d.object(forKey: "allowProvisioningUpdates") as? Bool ?? true
self.buildConfiguration = d.string(forKey: "buildConfiguration") ?? "Debug"
applyDockPolicy()
}
private func applyDockPolicy() {
DispatchQueue.main.async {
NSApp.setActivationPolicy(self.showInDock ? .regular : .accessory)
}
}
}
private extension Int {
var nonZero: Int? { self == 0 ? nil : self }
}
enum TailscaleDetector {
static func currentIP() -> String? {
let paths = [
"/Applications/Tailscale.app/Contents/MacOS/Tailscale",
"/usr/local/bin/tailscale",
"/opt/homebrew/bin/tailscale"
]
guard let exe = paths.first(where: { FileManager.default.fileExists(atPath: $0) }) else {
return nil
}
let p = Process()
p.executableURL = URL(fileURLWithPath: exe)
p.arguments = ["ip", "--4"]
let pipe = Pipe()
p.standardOutput = pipe
p.standardError = Pipe()
try? p.run(); p.waitUntilExit()
return String(data: pipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)?
.trimmingCharacters(in: .whitespacesAndNewlines)
.components(separatedBy: "\n").first
}
}