import Cocoa import AVFoundation import ApplicationServices import Network public final class AppDelegate: NSObject, NSApplicationDelegate { private var statusBarController: StatusBarController! private var audioRecorder = AudioRecorder() private var activeSession: SonioxLiveSession? private var isBusyFinalizing = false private var currentAudioLevel: Float = 0.0 private var latestPartialText: String? = nil private var permissionPollTimer: Timer? // Remote Android Phone Local Receiver (Port 8999) private var remoteListener: NWListener? public func applicationDidFinishLaunching(_ notification: Notification) { // Completely silent operation by default UserDefaults.standard.set(false, forKey: "SonioxPlaySounds") statusBarController = StatusBarController() statusBarController.onToggleRecording = { [weak self] in self?.toggleRecording() } // Push-To-Talk / Toggle Hotkey Setup HotkeyManager.shared.onHotkeyPressed = { [weak self] in guard let self = self else { return } if HotkeyManager.shared.currentMode == .pushToTalk { if !self.audioRecorder.isRecording { self.startRecording() } } else { self.toggleRecording() } } HotkeyManager.shared.onHotkeyReleased = { [weak self] in guard let self = self else { return } if HotkeyManager.shared.currentMode == .pushToTalk { if self.audioRecorder.isRecording { self.stopRecordingAndTranscribe() } } } audioRecorder.onAudioLevelUpdate = { [weak self] level in self?.currentAudioLevel = level } audioRecorder.onAudioChunkAvailable = { [weak self] chunk in self?.activeSession?.sendAudioChunk(chunk) } SonioxSessionPool.shared.prewarmNextSession() startRemotePasteServer() // Connect to Linux Persistent Gateway (via local tunnel 18999 -> 8999) RelayClient.shared.onRemoteUpdateReceived = { text, cursor, isFullReplace in print("AppDelegate: 📥 Clean Remote Input Update: '\(text.prefix(30))...' (replace: \(isFullReplace))") // Apply the remote update directly into the active input on Mac FocusedInputSync.shared.applyRemoteUpdate(text: text, cursor: cursor, isFullReplace: isFullReplace) } RelayClient.shared.start() HotkeyManager.shared.registerHotkeys() checkInitialPermissions() } private func startRemotePasteServer() { do { let port: NWEndpoint.Port = 8999 let listener = try NWListener(using: .tcp, on: port) listener.newConnectionHandler = { [weak self] connection in guard let self = self else { return } connection.start(queue: .main) self.handleRemoteConnection(connection) } listener.start(queue: .main) self.remoteListener = listener print("RemotePasteServer: Listening on 0.0.0.0:8999") } catch { print("RemotePasteServer: Failed to bind port 8999:", error) } } private func handleRemoteConnection(_ connection: NWConnection) { connection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { data, _, _, _ in guard let data = data, let reqStr = String(data: data, encoding: .utf8) else { connection.cancel() return } if reqStr.contains("GET /health") || reqStr.contains("GET /status") { let response = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\nContent-Length: 15\r\n\r\n{\"status\":\"ok\"}" connection.send(content: response.data(using: .utf8), completion: .contentProcessed({ _ in connection.cancel() })) return } if reqStr.contains("POST /paste") { if let bodyRange = reqStr.range(of: "\r\n\r\n") { let bodyJsonStr = String(reqStr[bodyRange.upperBound...]) if let bodyData = bodyJsonStr.data(using: .utf8), let json = try? JSONSerialization.jsonObject(with: bodyData) as? [String: Any], let text = json["text"] as? String { DispatchQueue.main.async { FocusedInputSync.shared.applyRemoteUpdate(text: text, cursor: nil, isFullReplace: true) } } } let response = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\nContent-Length: 19\r\n\r\n{\"status\":\"pasted\"}" connection.send(content: response.data(using: .utf8), completion: .contentProcessed({ _ in connection.cancel() })) return } connection.cancel() } } private func checkInitialPermissions() { // 1. Force macOS to trigger the system Accessibility prompt dialog! let axOptions = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] as CFDictionary let axTrusted = AXIsProcessTrustedWithOptions(axOptions) print("AppDelegate: 🔒 Accessibility Permission Status (trusted: \(axTrusted))") if !axTrusted { print("AppDelegate: ⚠️ Accessibility not yet granted! Prompting user and monitoring...") // Open Accessibility pane directly on both modern and legacy macOS DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { if let url1 = URL(string: "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Accessibility") { NSWorkspace.shared.open(url1) } if let url2 = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") { NSWorkspace.shared.open(url2) } } // Poll every 1.0 seconds until user toggles the switch permissionPollTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] timer in let nowTrusted = AXIsProcessTrusted() if nowTrusted { print("AppDelegate: 🟢 Accessibility Permission GRANTED by user!") timer.invalidate() self?.permissionPollTimer = nil HotkeyManager.shared.registerHotkeys() } } } // 2. Microphone permission audioRecorder.requestMicrophonePermission { granted in if !granted { print("Warning: Microphone permission not granted.") } } } public func toggleRecording() { if audioRecorder.isRecording { stopRecordingAndTranscribe() } else { startRecording() } } public func startRecording() { guard !audioRecorder.isRecording, !isBusyFinalizing else { return } latestPartialText = nil let session = SonioxSessionPool.shared.acquireSession() self.activeSession = session session.onPartialText = { [weak self] liveText in guard let self = self, self.audioRecorder.isRecording else { return } self.latestPartialText = liveText } session.onFinalResult = { [weak self] result in guard let self = self else { return } self.isBusyFinalizing = false self.activeSession = nil SonioxSessionPool.shared.prewarmNextSession() switch result { case .success(let text): FocusedInputSync.shared.applyRemoteUpdate(text: text, cursor: nil, isFullReplace: false) case .failure(let error): print("Soniox error:", error) } } do { try audioRecorder.startRecording() statusBarController.updateIcon(state: .recording) statusBarController.buildMenu(isRecording: true) } catch { print("Audio recording start error:", error) } } public func stopRecordingAndTranscribe() { guard audioRecorder.isRecording, !isBusyFinalizing else { return } isBusyFinalizing = true _ = audioRecorder.stopRecording() statusBarController.updateIcon(state: .transcribing) statusBarController.buildMenu(isRecording: false) guard let session = self.activeSession else { self.isBusyFinalizing = false return } session.finalizeStream() } }