import AppKit import Foundation // Simple .properties loader with bundle defaults and user override support final class Config { static let shared = Config() private var props: [String: String] = [:] private init() { // Defaults props = [ "svc.host": "svc.prole.org", "svc.port": "443", "k3s.retropie.host": "retropie.prole.org", "k3s.retropie.port": "6443", "k3d.local.host": "localhost", "k3d.local.port": "6443" ] // Load bundled defaults if present if let url = Bundle.main.url(forResource: "prole", withExtension: "properties") { merge(loadProperties(url: url)) } // Load user override from Application Support let fm = FileManager.default if let appSupport = try? fm.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: false) { let dir = appSupport.appendingPathComponent("Prole", isDirectory: true) let url = dir.appendingPathComponent("prole.properties") if fm.fileExists(atPath: url.path) { merge(loadProperties(url: url)) } } } private func merge(_ other: [String: String]) { for (k, v) in other { props[k] = v } } private func loadProperties(url: URL) -> [String: String] { guard let data = try? Data(contentsOf: url), let text = String(data: data, encoding: .utf8) else { return [:] } var result: [String: String] = [:] for line in text.components(separatedBy: .newlines) { let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines) if trimmed.isEmpty || trimmed.hasPrefix("#") { continue } if let eq = trimmed.firstIndex(of: "=") { let key = String(trimmed[.. String { props[key] ?? def } func int(_ key: String, default def: Int) -> Int { Int(props[key] ?? "") ?? def } // Convenience accessors used by ServiceChecker/StatusView var svcHost: String { string("svc.host", default: "svc.prole.org") } var svcPort: Int { int("svc.port", default: 443) } var retropieHost: String { string("k3s.retropie.host", default: "retropie.prole.org") } var retropiePort: Int { int("k3s.retropie.port", default: 6443) } var localHost: String { string("k3d.local.host", default: "localhost") } var localPort: Int { int("k3d.local.port", default: 6443) } // UI background config was removed along with the splash screen. // Dev port-forward supervision var pfEnabled: Bool { let v = (props["pf.enabled"] ?? "").lowercased() return v == "1" || v == "true" || v == "yes" || v == "on" } // Background/foreground mode for port-forward launcher // Defaults to background to honor daemon-style commands. var pfModeBackground: Bool { let v = (props["pf.mode"] ?? "background").lowercased() // Allow synonyms if v == "bg" || v == "background" || v == "daemon" { return true } if v == "fg" || v == "foreground" { return false } return true } // Keepalive wrapper for port-forward commands. If true, a shell loop will // keep restarting the child command on exit and keep the wrapper process alive. // Default: true (so UI remains stable and processes behave like daemons). var pfKeepAlive: Bool { let v = (props["pf.keepalive"] ?? "true").lowercased() return v == "1" || v == "true" || v == "yes" || v == "on" } // Enable verbose debug logging for PortForwardManager and related startup. // Default: false. Set pf.debug=true to print detailed diagnostics. var pfDebug: Bool { let v = (props["pf.debug"] ?? "false").lowercased() return v == "1" || v == "true" || v == "yes" || v == "on" } // Discover and adopt already-running matching port-forward processes at startup/reset // Default: true var pfDiscovery: Bool { let v = (props["pf.discovery"] ?? "true").lowercased() return v == "1" || v == "true" || v == "yes" || v == "on" } // Adopt existing matching processes instead of launching duplicates // Default: true var pfAdoptExisting: Bool { let v = (props["pf.adoptExisting"] ?? "true").lowercased() return v == "1" || v == "true" || v == "yes" || v == "on" } // Optional kubeconfig path to export as KUBECONFIG for child processes var kubeconfigPath: String? { let v = (props["kubeconfig.path"] ?? "").trimmingCharacters(in: .whitespacesAndNewlines) return v.isEmpty ? nil : v } var pfCommands: [String] { // Collect keys pf.1, pf.2, ... in ascending order let keys = props.keys .filter { $0.hasPrefix("pf.") } .compactMap { k -> (Int, String)? in let tail = k.dropFirst(3) if let idx = Int(tail) { return (idx, k) } return nil } .sorted { $0.0 < $1.0 } .map { $0.1 } return keys.compactMap { props[$0] } .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } .filter { !$0.isEmpty } } }