import AppKit import ConjureKit import ServiceManagement @MainActor final class SettingsWindowController: NSWindowController, NSWindowDelegate { private let shortcut: GlobalShortcut private let recorder = ShortcutRecorderButton(title: "Record Shortcut…", target: nil, action: nil) private let clearButton = NSButton(title: "Clear", target: nil, action: nil) private let statusLabel = NSTextField(wrappingLabelWithString: "") private let errorLabel = NSTextField(wrappingLabelWithString: "") private let loginToggle = NSButton(checkboxWithTitle: "Launch at login", target: nil, action: nil) private let loginStatus = NSTextField(wrappingLabelWithString: "") private let loginSettings = NSButton(title: "Open Login Items Settings…", target: nil, action: nil) init(shortcut: GlobalShortcut) { self.shortcut = shortcut let window = ShortcutSettingsWindow( contentRect: NSRect(x: 0, y: 0, width: 620, height: 640), styleMask: [.titled, .closable, .miniaturizable, .fullSizeContentView], backing: .buffered, defer: false ) window.title = "Conjure Settings" window.appearance = NSAppearance(named: .darkAqua) window.titlebarAppearsTransparent = true window.backgroundColor = .clear window.isOpaque = false window.isMovableByWindowBackground = true window.isReleasedWhenClosed = false window.center() super.init(window: window) window.delegate = self window.recorder = recorder configureContent(in: window) refresh() } required init?(coder: NSCoder) { fatalError("Conjure creates its settings window programmatically") } func present(error: String? = nil) { cancelRecording() if let error { errorLabel.stringValue = errorLabel.stringValue.isEmpty ? error : "\(error)\n\(errorLabel.stringValue)" } refresh() window?.deminiaturize(nil) showWindow(nil) NSApp.activate() window?.makeKeyAndOrderFront(nil) } func windowDidBecomeKey(_ notification: Notification) { refreshLogin() } func windowDidResignKey(_ notification: Notification) { cancelRecording() } func windowWillClose(_ notification: Notification) { cancelRecording() } private func configureContent(in window: NSWindow) { let heading = NSTextField(labelWithString: "Global Shortcut") heading.font = .systemFont(ofSize: NSFont.systemFontSize, weight: .semibold) recorder.bezelStyle = .rounded recorder.target = self recorder.action = #selector(beginRecording) recorder.setAccessibilityLabel("Record global shortcut") recorder.capture = { [weak self] event in self?.capture(event) } recorder.cancel = { [weak self] in self?.cancelRecording() } recorder.modifiersChanged = { [weak self] in self?.errorLabel.stringValue = "Press a key together with Command, Option, or Control. Escape cancels." } let commandSpaceButton = NSButton(title: "Use ⌘Space", target: self, action: #selector(useCommandSpace)) commandSpaceButton.bezelStyle = .rounded commandSpaceButton.setAccessibilityLabel("Use Command Space as the global shortcut") clearButton.bezelStyle = .rounded clearButton.target = self clearButton.action = #selector(clearShortcut) clearButton.setAccessibilityLabel("Clear global shortcut") let controls = NSStackView(views: [recorder, commandSpaceButton, clearButton]) controls.orientation = .horizontal controls.alignment = .centerY controls.spacing = 8 let instructions = NSTextField(wrappingLabelWithString: "Click the shortcut button, then press a key with Command, Option, or Control. Press Escape to cancel. The global shortcut is paused while recording." ) instructions.textColor = .secondaryLabelColor statusLabel.textColor = .secondaryLabelColor statusLabel.setAccessibilityLabel("Global shortcut registration status") errorLabel.textColor = .systemRed errorLabel.setAccessibilityLabel("Shortcut feedback") let spotlight = NSTextField(wrappingLabelWithString: "To use ⌘Space, first free it in System Settings → Keyboard → Keyboard Shortcuts → Spotlight: turn off or change “Show Spotlight search.” Check Input Sources for other reserved shortcuts. Conjure never changes these settings." ) spotlight.textColor = .secondaryLabelColor let keyboardSettings = NSButton( title: "Open Keyboard Settings…", target: self, action: #selector(openKeyboardSettings) ) keyboardSettings.bezelStyle = .rounded loginToggle.target = self loginToggle.action = #selector(toggleLogin) loginSettings.target = self loginSettings.action = #selector(openLoginSettings) loginSettings.bezelStyle = .rounded loginStatus.setAccessibilityLabel("Launch at login status") heading.font = .systemFont(ofSize: 20, weight: .semibold) heading.textColor = SunsetPalette.text for label in [instructions, statusLabel, spotlight, loginStatus] { label.textColor = SunsetPalette.secondaryText } for button in [recorder, commandSpaceButton, clearButton, keyboardSettings, loginToggle, loginSettings] { button.contentTintColor = SunsetPalette.accent } let loginHeading = NSTextField(labelWithString: "Startup") loginHeading.font = .systemFont(ofSize: 20, weight: .semibold) loginHeading.textColor = SunsetPalette.text let stack = NSStackView(views: [ heading, controls, instructions, statusLabel, errorLabel, spotlight, keyboardSettings, loginHeading, loginToggle, loginStatus, loginSettings ]) stack.orientation = .vertical stack.alignment = .leading stack.spacing = 12 stack.translatesAutoresizingMaskIntoConstraints = false let content = NSVisualEffectView() content.material = .hudWindow content.blendingMode = .behindWindow content.state = .active window.contentView = content let background = LauncherBackground(frame: content.bounds) background.autoresizingMask = [.width, .height] background.setAccessibilityElement(false) content.addSubview(background) content.addSubview(stack) NSLayoutConstraint.activate([ stack.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 24), stack.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -24), stack.topAnchor.constraint(equalTo: (window.contentLayoutGuide as! NSLayoutGuide).topAnchor, constant: 24), stack.bottomAnchor.constraint(lessThanOrEqualTo: content.bottomAnchor, constant: -24), recorder.widthAnchor.constraint(greaterThanOrEqualToConstant: 190) ]) for label in [instructions, statusLabel, errorLabel, spotlight, loginStatus] { label.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true } } private func refreshLogin() { let status = SMAppService.mainApp.status loginToggle.state = (status == .enabled || status == .requiresApproval) ? .on : .off loginSettings.isHidden = status != .requiresApproval switch status { case .enabled: loginStatus.stringValue = "Conjure starts in the background when you log in." case .requiresApproval: loginStatus.stringValue = "Allow Conjure in System Settings → General → Login Items & Extensions." case .notRegistered: loginStatus.stringValue = "Start Conjure automatically so your global shortcut is available." case .notFound: loginStatus.stringValue = "Install Conjure in Applications before enabling launch at login." @unknown default: loginStatus.stringValue = "Launch at login status is unavailable." } } @objc private func toggleLogin() { cancelRecording() do { if loginToggle.state == .on { try SMAppService.mainApp.register() } else { try SMAppService.mainApp.unregister() } refreshLogin() } catch { refreshLogin() loginStatus.stringValue = "Could not change launch at login: \(error.localizedDescription)" } } @objc private func openLoginSettings() { cancelRecording() SMAppService.openSystemSettingsLoginItems() } private func refresh() { refreshLogin() let name = shortcut.shortcut?.displayName recorder.title = recorder.isRecording ? "Type shortcut…" : (name ?? "Record Shortcut…") recorder.setAccessibilityValue(name ?? "Not set") if recorder.isRecording { statusLabel.stringValue = "Global shortcut paused while recording." } else if shortcut.isRegistered { statusLabel.stringValue = "Global shortcut is active." } else if shortcut.shortcut != nil { statusLabel.stringValue = "This shortcut is saved but is not active." } else { statusLabel.stringValue = "No global shortcut is configured." } } @objc private func beginRecording() { guard !recorder.isRecording else { return } errorLabel.stringValue = "" shortcut.suspend() recorder.isRecording = true refresh() if window?.makeFirstResponder(recorder) != true { cancelRecording() } } private func capture(_ event: NSEvent) { guard !event.isARepeat else { return } if event.keyCode == 53, event.modifierFlags.intersection([.command, .option, .control]).isEmpty { cancelRecording() return } guard let candidate = KeyboardShortcut(keyCode: event.keyCode, modifiers: event.modifierFlags) else { recorder.isRecording = false errorLabel.stringValue = "Choose a supported key with Command, Option, or Control. A modifier by itself is not a shortcut." restoreSavedShortcut() refresh() return } apply(candidate) } private func apply(_ candidate: KeyboardShortcut?) { let wasRecording = recorder.isRecording recorder.isRecording = false errorLabel.stringValue = "" do { try shortcut.setShortcut(candidate) } catch { errorLabel.stringValue = error.localizedDescription if wasRecording { restoreSavedShortcut() } } refresh() } private func cancelRecording() { guard recorder.isRecording else { return } recorder.isRecording = false errorLabel.stringValue = "" restoreSavedShortcut() refresh() } private func restoreSavedShortcut() { do { try shortcut.setShortcut(shortcut.shortcut) } catch { let message = "Could not reactivate the saved shortcut: \(error.localizedDescription)" errorLabel.stringValue = errorLabel.stringValue.isEmpty ? message : "\(errorLabel.stringValue)\n\(message)" } } @objc private func useCommandSpace() { apply(.commandSpace) } @objc private func clearShortcut() { apply(nil) } @objc private func openKeyboardSettings() { cancelRecording() let url = URL(string: "x-apple.systempreferences:com.apple.Keyboard-Settings.extension")! if !NSWorkspace.shared.open(url) { errorLabel.stringValue = "Could not open Keyboard settings. Open System Settings → Keyboard manually." } } } @MainActor private final class ShortcutSettingsWindow: NSWindow { weak var recorder: ShortcutRecorderButton? override func performKeyEquivalent(with event: NSEvent) -> Bool { if let recorder, recorder.isRecording, firstResponder === recorder { return recorder.performKeyEquivalent(with: event) } return super.performKeyEquivalent(with: event) } } @MainActor private final class ShortcutRecorderButton: NSButton { var isRecording = false var capture: ((NSEvent) -> Void)? var cancel: (() -> Void)? var modifiersChanged: (() -> Void)? override var acceptsFirstResponder: Bool { true } override func performKeyEquivalent(with event: NSEvent) -> Bool { guard isRecording, window?.firstResponder === self else { return super.performKeyEquivalent(with: event) } keyDown(with: event) return true } override func keyDown(with event: NSEvent) { guard isRecording else { super.keyDown(with: event) return } let activationModifiers: NSEvent.ModifierFlags = [.command, .option, .control] if event.keyCode == 48, event.modifierFlags.intersection(activationModifiers).isEmpty { if event.modifierFlags.contains(.shift) { window?.selectPreviousKeyView(nil) } else { window?.selectNextKeyView(nil) } return } capture?(event) } override func flagsChanged(with event: NSEvent) { if isRecording { modifiersChanged?() } else { super.flagsChanged(with: event) } } override func resignFirstResponder() -> Bool { let resigned = super.resignFirstResponder() if resigned, isRecording { cancel?() } return resigned } }