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 // 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 guard let self = self else { return } self.currentAudioLevel = level if self.audioRecorder.isRecording { HUDOverlayController.shared.show(state: .recording(level: level, liveText: self.latestPartialText)) } } 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))") // 1. FIRST apply the remote update directly into the active input while window is in pristine focus! FocusedInputSync.shared.applyRemoteUpdate(text: text, cursor: cursor, isFullReplace: isFullReplace) // 2. THEN show the floating success HUD notification DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { HUDOverlayController.shared.show(state: .success(text: text)) } } RelayClient.shared.start() 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() { 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 HUDOverlayController.shared.show(state: .recording(level: self.currentAudioLevel, liveText: 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) HUDOverlayController.shared.show(state: .error(message: error.localizedDescription)) } } do { try audioRecorder.startRecording() statusBarController.updateIcon(state: .recording) statusBarController.buildMenu(isRecording: true) HUDOverlayController.shared.show(state: .recording(level: 0.0, liveText: nil)) } catch { HUDOverlayController.shared.show(state: .error(message: error.localizedDescription)) } } public func stopRecordingAndTranscribe() { guard audioRecorder.isRecording, !isBusyFinalizing else { return } isBusyFinalizing = true _ = audioRecorder.stopRecording() statusBarController.updateIcon(state: .transcribing) statusBarController.buildMenu(isRecording: false) HUDOverlayController.shared.show(state: .transcribing) guard let session = self.activeSession else { self.isBusyFinalizing = false HUDOverlayController.shared.hide(animated: true) return } session.finalizeStream() } }