import Foundation import ObjCExceptionCatcher class IOBridge: NSObject { private let jsonEncoder: JSONEncoder private let jsonDecoder: JSONDecoder private let accessibilityService: AccessibilityService private let audioService: AudioService private let dateFormatter: DateFormatter init(jsonEncoder: JSONEncoder, jsonDecoder: JSONDecoder) { self.jsonEncoder = jsonEncoder self.jsonDecoder = jsonDecoder self.accessibilityService = AccessibilityService() self.audioService = AudioService() // Audio preloaded here at startup self.dateFormatter = DateFormatter() self.dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS" super.init() } private func logToStderr(_ message: String) { let timestamp = dateFormatter.string(from: Date()) let logMessage = "[\(timestamp)] \(message)\n" FileHandle.standardError.write(logMessage.data(using: .utf8)!) } // Handles a single RPC Request func handleRpcRequest(_ request: RPCRequestSchema) { var rpcResponse: RPCResponseSchema switch request.method { case .getAccessibilityTreeDetails: // Process accessibility tree requests on dedicated thread AccessibilityQueue.shared.async { [weak self] in guard let self = self else { return } self.handleAccessibilityTreeDetails(request) } return case .getAccessibilityContext: // Process accessibility context requests on dedicated thread AccessibilityQueue.shared.async { [weak self] in guard let self = self else { return } self.handleAccessibilityContext(request) } return case .pasteText: logToStderr("[IOBridge] Handling pasteText for ID: \(request.id)") guard let paramsAnyCodable = request.params else { let errPayload = Error( code: -32602, data: nil, message: "Missing params for pasteText") rpcResponse = RPCResponseSchema(error: errPayload, id: request.id, result: nil) sendRpcResponse(rpcResponse) return } do { let paramsData = try jsonEncoder.encode(paramsAnyCodable) // Corrected to use generated Swift model name from models.swift let pasteParams = try jsonDecoder.decode( PasteTextParamsSchema.self, from: paramsData) logToStderr("[IOBridge] Decoded pasteParams.transcript for ID: \(request.id)") // Call the actual paste function (to be implemented in AccessibilityService or similar) let success = accessibilityService.pasteText(transcript: pasteParams.transcript) // Corrected to use generated Swift model name from models.swift let resultPayload = PasteTextResultSchema( message: success ? "Pasted successfully" : "Paste failed", success: success) let resultData = try jsonEncoder.encode(resultPayload) let resultAsJsonAny = try jsonDecoder.decode(JSONAny.self, from: resultData) rpcResponse = RPCResponseSchema(error: nil, id: request.id, result: resultAsJsonAny) } catch { logToStderr( "[IOBridge] Error processing pasteText params or operation: \(error.localizedDescription) for ID: \(request.id)" ) let errPayload = Error( code: -32602, data: request.params, message: "Invalid params or error during paste: \(error.localizedDescription)") rpcResponse = RPCResponseSchema(error: errPayload, id: request.id, result: nil) } case .muteSystemAudio: logToStderr("[IOBridge] Handling muteSystemAudio for ID: \(request.id)") audioService.playSound(named: "rec-start") { [weak self] in guard let self = self else { let timestamp = DateFormatter().string(from: Date()) let logMessage = "[\(timestamp)] [IOBridge] self is nil in playSound completion for muteSystemAudio. ID: \(request.id)\n" FileHandle.standardError.write(logMessage.data(using: .utf8)!) return } self.logToStderr( "[IOBridge] rec-start.mp3 finished playing successfully. Proceeding to mute system audio. ID: \(request.id)" ) let success = self.accessibilityService.muteSystemAudio() let resultPayload = MuteSystemAudioResultSchema( message: success ? "Mute command sent" : "Failed to send mute command", success: success) var responseToSend: RPCResponseSchema do { let resultData = try self.jsonEncoder.encode(resultPayload) let resultAsJsonAny = try self.jsonDecoder.decode( JSONAny.self, from: resultData) responseToSend = RPCResponseSchema( error: nil, id: request.id, result: resultAsJsonAny) } catch { self.logToStderr( "[IOBridge] Error encoding muteSystemAudio result: \(error.localizedDescription) for ID: \(request.id)" ) let errPayload = Error( code: -32603, data: nil, message: "Error encoding result: \(error.localizedDescription)") responseToSend = RPCResponseSchema( error: errPayload, id: request.id, result: nil) } self.sendRpcResponse(responseToSend) } return case .restoreSystemAudio: logToStderr("[IOBridge] Handling restoreSystemAudio for ID: \(request.id)") let success = accessibilityService.restoreSystemAudio() if success { // Play sound only if restore was successful audioService.playSound(named: "rec-stop") } let resultPayload = RestoreSystemAudioResultSchema( message: success ? "Restore command sent" : "Failed to send restore command", success: success) do { let resultData = try jsonEncoder.encode(resultPayload) let resultAsJsonAny = try jsonDecoder.decode(JSONAny.self, from: resultData) rpcResponse = RPCResponseSchema(error: nil, id: request.id, result: resultAsJsonAny) } catch { logToStderr( "[IOBridge] Error encoding pauseSystemAudio result: \(error.localizedDescription) for ID: \(request.id)" ) let errPayload = Error( code: -32603, data: nil, message: "Error encoding result: \(error.localizedDescription)") rpcResponse = RPCResponseSchema(error: nil, id: request.id, result: nil) } case .setShortcuts: logToStderr("[IOBridge] Handling setShortcuts for ID: \(request.id)") guard let paramsAnyCodable = request.params else { let errPayload = Error( code: -32602, data: nil, message: "Missing params for setShortcuts") rpcResponse = RPCResponseSchema(error: errPayload, id: request.id, result: nil) sendRpcResponse(rpcResponse) return } do { let paramsData = try jsonEncoder.encode(paramsAnyCodable) let shortcutsParams = try jsonDecoder.decode( SetShortcutsParamsSchema.self, from: paramsData) // Update the ShortcutManager with the new shortcuts ShortcutManager.shared.setShortcuts( pushToTalk: shortcutsParams.pushToTalk, toggleRecording: shortcutsParams.toggleRecording ) let resultPayload = SetShortcutsResultSchema(success: true) let resultData = try jsonEncoder.encode(resultPayload) let resultAsJsonAny = try jsonDecoder.decode(JSONAny.self, from: resultData) rpcResponse = RPCResponseSchema(error: nil, id: request.id, result: resultAsJsonAny) } catch { logToStderr( "[IOBridge] Error processing setShortcuts params: \(error.localizedDescription) for ID: \(request.id)" ) let errPayload = Error( code: -32602, data: request.params, message: "Invalid params: \(error.localizedDescription)") rpcResponse = RPCResponseSchema(error: errPayload, id: request.id, result: nil) } default: logToStderr("[IOBridge] Method not found: \(request.method) for ID: \(request.id)") let errPayload = Error( code: -32601, data: nil, message: "Method not found: \(request.method)") rpcResponse = RPCResponseSchema(error: errPayload, id: request.id, result: nil) } sendRpcResponse(rpcResponse) } private func sendRpcResponse(_ response: RPCResponseSchema) { do { let responseData = try jsonEncoder.encode(response) if let responseString = String(data: responseData, encoding: .utf8) { logToStderr("[Swift Biz Logic] FINAL JSON RESPONSE to stdout: \(responseString)") print(responseString) fflush(stdout) } } catch { logToStderr("Error encoding RpcResponse: \(error.localizedDescription)") } } // Main loop for processing RPC requests from stdin func processRpcRequests() { logToStderr("IOBridge: Starting RPC request processing loop.") while let line = readLine(strippingNewline: true) { guard !line.isEmpty, let data = line.data(using: .utf8) else { logToStderr("Warning: Received empty or non-UTF8 line on stdin.") continue } do { let rpcRequest = try jsonDecoder.decode(RPCRequestSchema.self, from: data) logToStderr( "IOBridge: Received RPC Request ID \(rpcRequest.id), Method: \(rpcRequest.method)" ) handleRpcRequest(rpcRequest) } catch { logToStderr( "Error decoding RpcRequest from stdin: \(error.localizedDescription). Line: \(line)" ) // Consider sending a parse error if ID can be extracted } } logToStderr("IOBridge: RPC request processing loop finished (stdin closed).") } // MARK: - Async RPC Handlers private func handleAccessibilityTreeDetails(_ request: RPCRequestSchema) { var accessibilityParams: GetAccessibilityTreeDetailsParamsSchema? = nil logToStderr("[IOBridge] Handling getAccessibilityTreeDetails for ID: \(request.id)") if let paramsAnyCodable = request.params { do { let paramsData = try jsonEncoder.encode(paramsAnyCodable) accessibilityParams = try jsonDecoder.decode( GetAccessibilityTreeDetailsParamsSchema.self, from: paramsData) logToStderr( "[IOBridge] Decoded accessibilityParams.rootID: \(accessibilityParams?.rootID ?? "nil") for ID: \(request.id)" ) } catch { logToStderr( "[IOBridge] Error decoding getAccessibilityTreeDetails params: \(error.localizedDescription)" ) let errPayload = Error( code: -32602, data: request.params, message: "Invalid params: \(error.localizedDescription)") let rpcResponse = RPCResponseSchema(error: errPayload, id: request.id, result: nil) sendRpcResponse(rpcResponse) return } } // Fetch REAL accessibility tree data using the service switch ExceptionCatcher.try({ self.accessibilityService.fetchFullAccessibilityTree(rootId: accessibilityParams?.rootID) }) { case .success(let actualTreeData): logToStderr("[IOBridge] Fetched actualTreeData. Is nil? \(actualTreeData == nil). For ID: \(request.id)") var treeAsJsonAny: JSONAny? = nil if let dataToEncode = actualTreeData { do { let encodedData = try jsonEncoder.encode(dataToEncode) treeAsJsonAny = try jsonDecoder.decode(JSONAny.self, from: encodedData) } catch { logToStderr("[IOBridge] Error encoding actualTreeData: \(error.localizedDescription)") } } let resultPayload = GetAccessibilityTreeDetailsResultSchema(tree: treeAsJsonAny) var resultAsJsonAny: JSONAny? = nil do { let resultPayloadData = try jsonEncoder.encode(resultPayload) resultAsJsonAny = try jsonDecoder.decode(JSONAny.self, from: resultPayloadData) } catch { logToStderr("Error encoding result: \(error.localizedDescription)") } let rpcResponse = RPCResponseSchema(error: nil, id: request.id, result: resultAsJsonAny) sendRpcResponse(rpcResponse) case .exception(let exception): logToStderr("[IOBridge] NSException in fetchFullAccessibilityTree: \(exception.name) - \(exception.reason)") let exceptionData: [String: Any] = [ "name": exception.name, "reason": exception.reason, "callStack": exception.callStack.prefix(10).joined(separator: "\n") ] var exceptionJsonAny: JSONAny? = nil if let jsonData = try? JSONSerialization.data(withJSONObject: exceptionData), let decoded = try? jsonDecoder.decode(JSONAny.self, from: jsonData) { exceptionJsonAny = decoded } let errPayload = Error( code: -32603, data: exceptionJsonAny, message: "\(exception.name): \(exception.reason)" ) let rpcResponse = RPCResponseSchema(error: errPayload, id: request.id, result: nil) sendRpcResponse(rpcResponse) } } private func handleAccessibilityContext(_ request: RPCRequestSchema) { var contextParams: GetAccessibilityContextParamsSchema? = nil logToStderr("[IOBridge] Handling getAccessibilityContext for ID: \(request.id)") if let paramsAnyCodable = request.params { do { let paramsData = try jsonEncoder.encode(paramsAnyCodable) contextParams = try jsonDecoder.decode( GetAccessibilityContextParamsSchema.self, from: paramsData) logToStderr( "[IOBridge] Decoded contextParams.editableOnly: \(contextParams?.editableOnly ?? false) for ID: \(request.id)" ) } catch { logToStderr( "[IOBridge] Error decoding getAccessibilityContext params: \(error.localizedDescription)" ) let errPayload = Error( code: -32602, data: request.params, message: "Invalid params: \(error.localizedDescription)") let rpcResponse = RPCResponseSchema(error: errPayload, id: request.id, result: nil) sendRpcResponse(rpcResponse) return } } let editableOnly = contextParams?.editableOnly ?? false switch ExceptionCatcher.try({ AccessibilityContextService.getAccessibilityContext(editableOnly: editableOnly) }) { case .success(let context): logToStderr("[IOBridge] Retrieved context for ID: \(request.id)") let resultPayload = GetAccessibilityContextResultSchema(context: context) do { let resultData = try jsonEncoder.encode(resultPayload) let resultAsJsonAny = try jsonDecoder.decode(JSONAny.self, from: resultData) let rpcResponse = RPCResponseSchema(error: nil, id: request.id, result: resultAsJsonAny) sendRpcResponse(rpcResponse) } catch { logToStderr("[IOBridge] Error encoding result: \(error.localizedDescription) for ID: \(request.id)") let errPayload = Error(code: -32603, data: nil, message: "Error encoding result: \(error.localizedDescription)") let rpcResponse = RPCResponseSchema(error: errPayload, id: request.id, result: nil) sendRpcResponse(rpcResponse) } case .exception(let exception): logToStderr("[IOBridge] NSException in getAccessibilityContext: \(exception.name) - \(exception.reason)") let exceptionData: [String: Any] = [ "name": exception.name, "reason": exception.reason, "callStack": exception.callStack.prefix(10).joined(separator: "\n") ] var exceptionJsonAny: JSONAny? = nil if let jsonData = try? JSONSerialization.data(withJSONObject: exceptionData), let decoded = try? jsonDecoder.decode(JSONAny.self, from: jsonData) { exceptionJsonAny = decoded } let errPayload = Error( code: -32603, data: exceptionJsonAny, message: "\(exception.name): \(exception.reason)" ) let rpcResponse = RPCResponseSchema(error: errPayload, id: request.id, result: nil) sendRpcResponse(rpcResponse) } } }