import SwiftUI import UIKit struct SettingsView: View { @EnvironmentObject var daemonConfig: DaemonConfiguration @ObservedObject var editorState: EditorState @ObservedObject var projectStore: ProjectStore @State private var pendingHost = "" @State private var pendingPort = "" @State private var wcApiKey = "" @State private var connStatus: ConnStatus = .untested @State private var showAddProject = false enum ConnStatus { case untested, testing, success, failure(String) var label: String { switch self { case .untested: return "Not tested"; case .testing: return "Testing…"; case .success: return "Connected ✓"; case .failure(let m): return "Failed: \(m)" } } var color: Color { switch self { case .success: return .green; case .failure: return .red; case .testing: return .secondary; default: return .secondary } } static func ==(a: ConnStatus, b: ConnStatus) -> Bool { switch (a,b) { case (.success,.success),(.untested,.untested),(.testing,.testing): return true; case (.failure(let x),.failure(let y)): return x==y; default: return false } } } var body: some View { NavigationStack { Form { // ── Daemon ──────────────────────────────────────────────── Section { HStack { Label("Tailscale IP", systemImage:"network"); Spacer() TextField("100.x.x.x", text:$pendingHost).multilineTextAlignment(.trailing).keyboardType(.numbersAndPunctuation).autocorrectionDisabled().autocapitalization(.none).frame(width:160).onChange(of:pendingHost){_,_ in connStatus = .untested} } HStack { Label("Port", systemImage:"antenna.radiowaves.left.and.right"); Spacer() TextField("8080", text:$pendingPort).multilineTextAlignment(.trailing).keyboardType(.numberPad).frame(width:80) } HStack { Text("Status").foregroundStyle(.secondary); Spacer(); Text(connStatus.label).foregroundStyle(connStatus.color).font(.subheadline) } Button("Test Connection") { Task { await testConnection() } }.disabled(pendingHost.isEmpty) Button("Save & Apply") { applyConnection() }.buttonStyle(.borderedProminent) .disabled(pendingHost.isEmpty || connStatus != .success) } header: { Text("Mac Daemon") } // ── Projects ────────────────────────────────────────────── Section { ForEach(projectStore.projects) { p in VStack(alignment:.leading, spacing:2) { Text(p.name).font(.body) Text(p.rootPath).font(.caption).foregroundStyle(.secondary).lineLimit(1).truncationMode(.middle) Text("Scheme: \(p.scheme) · WC: \(p.workingCopyRepoName.isEmpty ? "not set" : p.workingCopyRepoName)").font(.caption2).foregroundStyle(.tertiary) }.padding(.vertical,2) }.onDelete { projectStore.remove(atOffsets:$0) } Button { showAddProject = true } label: { Label("Add Project", systemImage:"plus.circle") } } header: { Text("Projects") } footer: { Text("Mac path should be the directory containing project.yml or .xcodeproj") } // ── Editor ──────────────────────────────────────────────── Section("Editor Appearance") { HStack { Label("Font Size", systemImage:"textformat.size"); Spacer() Stepper("\(Int(editorState.fontSize))pt", value:$editorState.fontSize, in:8...32, step:1).frame(width:160) } Picker(selection:$editorState.selectedTheme) { ForEach(EditorThemeOption.allCases) { o in Text(o.displayName).tag(o) } } label: { Label("Theme", systemImage:"paintbrush") } Toggle(isOn:$editorState.showLineNumbers) { Label("Line Numbers", systemImage:"list.number") } Toggle(isOn:$editorState.lineWrapping) { Label("Line Wrapping", systemImage:"text.word.spacing") } HStack { Label("Indent Width", systemImage:"arrow.right.to.line"); Spacer() Picker("", selection:$editorState.indentWidth) { Text("2 spaces").tag(2); Text("4 spaces").tag(4); Text("Tab").tag(0) }.labelsHidden().pickerStyle(.menu) } } // ── Build ───────────────────────────────────────────────── Section("Build") { HStack { Label("Team ID", systemImage:"person.badge.key"); Spacer() TextField("XXXXXXXXXX", text:$daemonConfig.developmentTeam).multilineTextAlignment(.trailing).autocorrectionDisabled().autocapitalization(.none).frame(width:120) } Toggle(isOn:$daemonConfig.allowProvisioningUpdates) { Label("Allow Provisioning Updates", systemImage:"checkmark.seal") } Toggle(isOn:$daemonConfig.buildInRelease) { Label("Build in Release", systemImage:"bolt") } } // ── Working Copy ────────────────────────────────────────── Section { HStack { Label("API Key", systemImage:"key.fill"); Spacer() SecureField("Paste from Working Copy", text:$wcApiKey).multilineTextAlignment(.trailing).autocorrectionDisabled().autocapitalization(.none).frame(width:200) .onChange(of:wcApiKey){_,k in UserDefaults.standard.set(k, forKey:"workingCopyAPIKey")} } let installed = UIApplication.shared.canOpenURL(URL(string:"working-copy://")!) Label(installed ? "Working Copy detected ✓" : "Working Copy not installed", systemImage: installed ? "checkmark.circle.fill" : "exclamationmark.triangle") .foregroundStyle(installed ? .green : .orange).font(.caption) Text("For local git over Tailscale, clone in Working Copy using:\nssh://user@100.x.x.x/path/to/repo.git") .font(.caption).foregroundStyle(.secondary) } header: { Text("Working Copy (Git)") } // ── LSP ─────────────────────────────────────────────────── Section { Toggle(isOn:$daemonConfig.lspEnabled) { Label("sourcekit-lsp Proxy", systemImage:"wand.and.stars") } if daemonConfig.lspEnabled { HStack { Label("Status", systemImage:"circle.fill").foregroundStyle(daemonConfig.lspConnected ? Color.green : Color.red) Spacer() Text(daemonConfig.lspConnected ? "Running" : "Disconnected").foregroundStyle(.secondary) } } } header: { Text("Language Server (LSP)") } // ── About ───────────────────────────────────────────────── Section("About") { LabeledContent("Version", value:"1.0.0-alpha") LabeledContent("Editor Engine", value:"Runestone 0.5.1 / Tree-sitter") LabeledContent("Protocol", value:"REST + WebSocket") } } .navigationTitle("Settings").navigationBarTitleDisplayMode(.inline) .onAppear { pendingHost = daemonConfig.host; pendingPort = String(daemonConfig.port) wcApiKey = UserDefaults.standard.string(forKey:"workingCopyAPIKey") ?? "" } .sheet(isPresented:$showAddProject) { AddProjectView(projectStore:projectStore) } } } private func testConnection() async { connStatus = .testing let host = pendingHost.trimmingCharacters(in:.whitespaces) let port = Int(pendingPort) ?? 8080 guard let url = URL(string:"http://\(host):\(port)/health") else { connStatus = .failure("Invalid URL"); return } var req = URLRequest(url:url); req.timeoutInterval = 4 do { let (_,resp) = try await URLSession.shared.data(for:req) connStatus = (resp as? HTTPURLResponse)?.statusCode == 200 ? .success : .failure("Unexpected response") } catch { connStatus = .failure(error.localizedDescription) } } private func applyConnection() { daemonConfig.host = pendingHost.trimmingCharacters(in:.whitespaces) daemonConfig.port = Int(pendingPort) ?? 8080 } } // MARK: - Add Project View struct AddProjectView: View { @ObservedObject var projectStore: ProjectStore @Environment(\.dismiss) var dismiss @State private var name = ""; @State private var path = "" @State private var scheme = ""; @State private var wcRepo = "" var body: some View { NavigationStack { Form { Section("Project Details") { TextField("Name (e.g. NavidromePlayer)", text:$name) TextField("/Users/dallas/Dev/NavidromePlayer/NavidromePlayer", text:$path).autocorrectionDisabled().autocapitalization(.none).keyboardType(.URL) TextField("Xcode Scheme", text:$scheme).autocorrectionDisabled().autocapitalization(.none) } Section("Working Copy") { TextField("Repo name in Working Copy (e.g. NavidromePlayer)", text:$wcRepo).autocorrectionDisabled().autocapitalization(.none) } footer: { Text("Must match the display name in Working Copy's repo list.") } } .navigationTitle("Add Project").navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement:.cancellationAction) { Button("Cancel") { dismiss() } } ToolbarItem(placement:.confirmationAction) { Button("Add") { projectStore.add(SavedProject(name: name.isEmpty ? scheme : name, rootPath: path, scheme: scheme, workingCopyRepoName: wcRepo)) dismiss() }.disabled(path.isEmpty || scheme.isEmpty) } } }.presentationDetents([.medium]) } }