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 // Equatable conformance added so != operator works on line 39 and elsewhere. enum ConnStatus: Equatable { case untested, testing, success, failure(String) var label: String { switch self { case .untested: return "Not tested" case .testing: return "Testing..." case .success: return "Connected checkmark" case .failure(let m): return "Failed: \(m)" } } var color: Color { switch self { case .success: return .green case .failure: return .red default: return .secondary } } // Custom == required because .failure has an associated value. 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) // != now works because ConnStatus conforms to Equatable. .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() // fontSizeStored is the @AppStorage Double backing fontSize. // Binding to the computed fontSize property is not possible; // bind to the stored Double and display with Int cast. Stepper( "\(Int(editorState.fontSizeStored))pt", value: $editorState.fontSizeStored, in: 8.0...32.0, step: 1.0 ) .frame(width: 160) } // selectedTheme is a computed property on EditorState — // cannot use $ directly; use an explicit Binding. Picker(selection: Binding( get: { editorState.selectedTheme }, set: { editorState.selectedTheme = $0 } )) { 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) } } } // MARK: - Actions 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 status code") } 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 with both header string AND footer requires the // explicit header:/footer: trailing closure form. Section { TextField("Repo name in Working Copy (e.g. NavidromePlayer)", text: $wcRepo) .autocorrectionDisabled() .autocapitalization(.none) } header: { Text("Working Copy") } 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]) } }