// // CommandPalette.swift // Zyquo Router // // Author: Simon-Pierre Boucher // Mail: contact@spboucher.ai // // ⌘K: fuzzy access to everything — start/stop, sections, copy endpoint, // and copy any model ID. Return runs the highlighted action. // import SwiftUI struct CommandPalette: View { @Binding var isPresented: Bool @EnvironmentObject private var server: ServerController @EnvironmentObject private var catalog: ModelCatalog @State private var query = "" @State private var highlighted = 0 @FocusState private var fieldFocused: Bool private struct Action: Identifiable { let id: String let title: String let subtitle: String? let systemImage: String let run: () -> Void } private var actions: [Action] { var all: [Action] = [ Action( id: "server", title: server.isRunning ? "Stop Server" : "Start Server", subtitle: server.isRunning ? "Port \(server.port)" : nil, systemImage: "power", run: { server.toggle() } ), Action( id: "copy-endpoint", title: "Copy Endpoint URL", subtitle: server.endpointURL, systemImage: "doc.on.doc", run: { NSPasteboard.general.clearContents() NSPasteboard.general.setString(server.endpointURL, forType: .string) } ), ] for section in AppSection.allCases { all.append(Action( id: "go-\(section.rawValue)", title: "Go to \(section.rawValue)", subtitle: nil, systemImage: section.systemImage, run: { UserDefaults.standard.set(section.rawValue, forKey: "selectedSection") } )) } for model in catalog.all { let id = RequestRouter.namespacedID(for: model) all.append(Action( id: "model-\(id)", title: id, subtitle: "Copy model ID", systemImage: "square.grid.2x2", run: { NSPasteboard.general.clearContents() NSPasteboard.general.setString(id, forType: .string) } )) } return all } private var filtered: [Action] { let trimmed = query.trimmingCharacters(in: .whitespaces) guard !trimmed.isEmpty else { return Array(actions.prefix(9)) } return Array(actions.filter { $0.title.localizedCaseInsensitiveContains(trimmed) }.prefix(9)) } var body: some View { VStack(spacing: 0) { TextField("Type a command or model…", text: $query) .textFieldStyle(.plain) .font(ZyquoFont.body(size: 15)) .padding(ZyquoSpacing.md) .focused($fieldFocused) .onSubmit { runHighlighted() } .onChange(of: query) { _ in highlighted = 0 } ZyquoHairline() VStack(spacing: 0) { ForEach(Array(filtered.enumerated()), id: \.element.id) { index, action in HStack(spacing: ZyquoSpacing.sm) { Image(systemName: action.systemImage) .font(.system(size: 12)) .foregroundStyle(index == highlighted ? ZyquoColor.accent : ZyquoColor.textSecondary) .frame(width: 18) Text(action.title) .font(action.id.hasPrefix("model-") ? ZyquoFont.mono(size: 12.5) : ZyquoFont.body()) .foregroundStyle(ZyquoColor.textPrimary) .lineLimit(1) .truncationMode(.middle) Spacer() if let subtitle = action.subtitle { Text(subtitle) .font(ZyquoFont.caption) .foregroundStyle(ZyquoColor.textTertiary) .lineLimit(1) } } .padding(.horizontal, ZyquoSpacing.md) .padding(.vertical, ZyquoSpacing.xs) .background(index == highlighted ? ZyquoColor.accentSubtle : .clear) .contentShape(Rectangle()) .onTapGesture { action.run() isPresented = false } .onHover { hover in if hover { highlighted = index } } } } .padding(.vertical, ZyquoSpacing.xxs) } .frame(width: 520) .background( RoundedRectangle(cornerRadius: ZyquoRadius.large, style: .continuous) .fill(ZyquoColor.surface) .overlay( RoundedRectangle(cornerRadius: ZyquoRadius.large, style: .continuous) .strokeBorder(ZyquoColor.border, lineWidth: ZyquoMetrics.hairline) ) ) .zyquoSoftShadow() .onAppear { fieldFocused = true } .background(paletteKeyHandlers) } /// Hidden buttons carrying the arrow-key/escape shortcuts. private var paletteKeyHandlers: some View { Group { Button("") { highlighted = min(highlighted + 1, filtered.count - 1) } .keyboardShortcut(.downArrow, modifiers: []) Button("") { highlighted = max(highlighted - 1, 0) } .keyboardShortcut(.upArrow, modifiers: []) Button("") { isPresented = false } .keyboardShortcut(.escape, modifiers: []) } .hidden() } private func runHighlighted() { guard filtered.indices.contains(highlighted) else { return } filtered[highlighted].run() isPresented = false } }