Soniox Mobile to Mac - Real-time Voice Dictation & Remote Input Control
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

384 lines
15 KiB

import Cocoa
import ApplicationServices
import CoreGraphics
public struct MacInputState: Codable {
public let source: String
public let app: String
public let text: String
public let cursor: Int
public let selection: Int
public let revision: Int64
public let timestamp: Double
public init(app: String, text: String, cursor: Int, selection: Int, revision: Int64) {
self.source = "mac"
self.app = app
self.text = text
self.cursor = cursor
self.selection = selection
self.revision = revision
self.timestamp = Date().timeIntervalSince1970
}
}
public final class FocusedInputSync {
public static let shared = FocusedInputSync()
private let systemWideElement: AXUIElement
private var isApplyingRemoteChange: Bool = false
private var lastObservedText: String = ""
private var lastObservedCursor: Int = -1
private var lastObservedApp: String = ""
private var localRevision: Int64 = 0
private var remoteChangeExpiryTime: Double = 0
private init() {
self.systemWideElement = AXUIElementCreateSystemWide()
enableGlobalAccessibility()
}
private func enableGlobalAccessibility() {
AXUIElementSetAttributeValue(systemWideElement, "AXEnhancedUserInterface" as CFString, kCFBooleanTrue)
AXUIElementSetAttributeValue(systemWideElement, "AXManualAccessibility" as CFString, kCFBooleanTrue)
}
/// Resolves the actual user-facing application, bypassing system overlays
public func getRealFrontmostApp() -> NSRunningApplication? {
if let front = NSWorkspace.shared.frontmostApplication,
front.activationPolicy == .regular,
let bundleId = front.bundleIdentifier,
!bundleId.contains("notificationcenter"),
!bundleId.contains("controlcenter"),
!bundleId.contains("WindowManager"),
!bundleId.contains("Soniox") {
return front
}
let windowList = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] ?? []
for win in windowList {
let layer = win[kCGWindowLayer as String] as? Int ?? -1
let pid = win[kCGWindowOwnerPID as String] as? pid_t ?? 0
let bounds = win[kCGWindowBounds as String] as? [String: Any] ?? [:]
let width = bounds["Width"] as? CGFloat ?? 0
let height = bounds["Height"] as? CGFloat ?? 0
if layer == 0 && width > 100 && height > 100 {
if let app = NSRunningApplication(processIdentifier: pid),
app.activationPolicy == .regular,
let bundleId = app.bundleIdentifier,
!bundleId.contains("notificationcenter"),
!bundleId.contains("controlcenter"),
!bundleId.contains("WindowManager"),
!bundleId.contains("Soniox") {
return app
}
}
}
return NSWorkspace.shared.frontmostApplication
}
private func findFocusedDescendant(_ elem: AXUIElement, depth: Int = 0) -> AXUIElement? {
if depth > 12 { return nil }
var isFocusedObj: CFTypeRef?
if AXUIElementCopyAttributeValue(elem, kAXFocusedAttribute as CFString, &isFocusedObj) == .success,
let isFocused = isFocusedObj as? Bool, isFocused {
var roleObj: CFTypeRef?
AXUIElementCopyAttributeValue(elem, kAXRoleAttribute as CFString, &roleObj)
let role = (roleObj as? String) ?? ""
if role != "AXWindow" && role != "AXApplication" && role != "AXGroup" && role != "AXSplitGroup" && role != "AXScrollArea" {
return elem
}
}
var childrenObj: CFTypeRef?
if AXUIElementCopyAttributeValue(elem, kAXChildrenAttribute as CFString, &childrenObj) == .success,
let children = childrenObj as? [AXUIElement] {
for child in children {
if let found = findFocusedDescendant(child, depth: depth + 1) {
return found
}
}
}
return nil
}
public func getFocusedElement() -> (AXUIElement?, String) {
guard let frontApp = getRealFrontmostApp() else { return (nil, "App") }
let appName = frontApp.localizedName ?? "App"
let appElem = AXUIElementCreateApplication(frontApp.processIdentifier)
AXUIElementSetAttributeValue(appElem, "AXEnhancedUserInterface" as CFString, kCFBooleanTrue)
AXUIElementSetAttributeValue(appElem, "AXManualAccessibility" as CFString, kCFBooleanTrue)
var targetElem: AXUIElement?
// 1. System-wide focused element
var focusedObj: CFTypeRef?
if AXUIElementCopyAttributeValue(systemWideElement, kAXFocusedUIElementAttribute as CFString, &focusedObj) == .success,
let obj = focusedObj {
let elem = obj as! AXUIElement
var roleObj: CFTypeRef?
AXUIElementCopyAttributeValue(elem, kAXRoleAttribute as CFString, &roleObj)
let role = (roleObj as? String) ?? ""
if role != "AXWindow" && role != "AXApplication" {
targetElem = elem
}
}
// 2. App focused element
if targetElem == nil {
if AXUIElementCopyAttributeValue(appElem, kAXFocusedUIElementAttribute as CFString, &focusedObj) == .success,
let obj = focusedObj {
let elem = obj as! AXUIElement
var roleObj: CFTypeRef?
AXUIElementCopyAttributeValue(elem, kAXRoleAttribute as CFString, &roleObj)
let role = (roleObj as? String) ?? ""
if role != "AXWindow" && role != "AXApplication" {
targetElem = elem
}
}
}
// 3. App focused window element
if targetElem == nil {
var focusedWinObj: CFTypeRef?
if AXUIElementCopyAttributeValue(appElem, kAXFocusedWindowAttribute as CFString, &focusedWinObj) == .success,
let win = focusedWinObj {
var winFocObj: CFTypeRef?
if AXUIElementCopyAttributeValue((win as! AXUIElement), kAXFocusedUIElementAttribute as CFString, &winFocObj) == .success,
let obj = winFocObj {
targetElem = (obj as! AXUIElement)
}
}
}
// 4. Recursive search in tree
if targetElem == nil {
targetElem = findFocusedDescendant(appElem)
}
return (targetElem, appName)
}
/// Inspects current focused element on Mac and returns state snapshot if user typed on Mac
public func inspectCurrentState() -> MacInputState? {
let now = Date().timeIntervalSince1970
if isApplyingRemoteChange || now < remoteChangeExpiryTime {
return nil
}
let (elemOpt, appName) = getFocusedElement()
let appChanged = (appName != lastObservedApp)
if appChanged {
lastObservedApp = appName
}
guard let elem = elemOpt else {
if appChanged && !lastObservedText.isEmpty {
localRevision += 1
return MacInputState(app: appName, text: lastObservedText, cursor: lastObservedCursor, selection: 0, revision: localRevision)
}
return nil
}
var roleObj: CFTypeRef?
AXUIElementCopyAttributeValue(elem, kAXRoleAttribute as CFString, &roleObj)
let role = roleObj as? String ?? ""
let isTextRole = (role == "AXTextField" || role == "AXTextArea" || role == "AXComboBox" || role == "AXSearchField")
var text = ""
var valObj: CFTypeRef?
if AXUIElementCopyAttributeValue(elem, kAXValueAttribute as CFString, &valObj) == .success,
let val = valObj {
if let str = val as? String {
text = str
} else if let attrStr = val as? NSAttributedString {
text = attrStr.string
}
}
if text.isEmpty && isTextRole {
var countObj: CFTypeRef?
if AXUIElementCopyAttributeValue(elem, kAXNumberOfCharactersAttribute as CFString, &countObj) == .success,
let count = countObj as? Int, count > 0 {
var range = CFRange(location: 0, length: count)
if let axRange = AXValueCreate(.cfRange, &range) {
var strObj: CFTypeRef?
if AXUIElementCopyParameterizedAttributeValue(elem, kAXStringForRangeParameterizedAttribute as CFString, axRange, &strObj) == .success,
let str = strObj as? String {
text = str
}
}
}
}
// Ghost empty suppression for non-native containers
if text.isEmpty && !lastObservedText.isEmpty && !isTextRole {
return nil
}
var cursor = text.count
var selLen = 0
var rangeObj: CFTypeRef?
if AXUIElementCopyAttributeValue(elem, kAXSelectedTextRangeAttribute as CFString, &rangeObj) == .success,
let axRange = rangeObj {
var range = CFRange()
if AXValueGetValue(axRange as! AXValue, .cfRange, &range) {
cursor = range.location
selLen = range.length
}
}
if text == lastObservedText && cursor == lastObservedCursor && !appChanged {
return nil
}
lastObservedText = text
lastObservedCursor = cursor
lastObservedApp = appName
localRevision += 1
return MacInputState(app: appName, text: text, cursor: cursor, selection: selLen, revision: localRevision)
}
/// Perfectly mirrors the full text to the active Mac input box in real-time
@discardableResult
public func applyRemoteUpdate(text: String, cursor: Int? = nil, isFullReplace: Bool = true) -> Bool {
isApplyingRemoteChange = true
remoteChangeExpiryTime = Date().timeIntervalSince1970 + 0.25
defer {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
self.isApplyingRemoteChange = false
}
}
// Normalize line endings and preserve intentional multiline text (\n)
var cleanText = text.replacingOccurrences(of: "\r\n", with: "\n").replacingOccurrences(of: "\r", with: "\n")
cleanText = cleanText.trimmingCharacters(in: .whitespacesAndNewlines)
let targetCursor = cursor ?? cleanText.count
lastObservedText = cleanText
lastObservedCursor = targetCursor
let (_, appName) = getFocusedElement()
lastObservedApp = appName
// Universal Quartz CGEvent Keystroke Engine (Cmd+A -> Cmd+V / Backspace or pure Cmd+V)
// Works 100% reliably across native, web, and Electron/Chromium apps (e.g. Antigravity, VS Code, Slack, Firefox)
print("FocusedInputSync: 🚀 Injecting text into '\(appName)' (replace: \(isFullReplace), len: \(cleanText.count), lines: \(cleanText.components(separatedBy: "\n").count))")
if isFullReplace {
return executeCleanFullReplace(cleanText)
} else {
return pasteOnlyViaCleanKeystroke(cleanText)
}
}
/**
* Inserts speech directly at active cursor via clean Cmd+V
*/
private func pasteOnlyViaCleanKeystroke(_ text: String) -> Bool {
guard !text.isEmpty else { return true }
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
pasteboard.setString(text, forType: .string)
let src = CGEventSource(stateID: .hidSystemState)
let kVK_ANSI_V: CGKeyCode = 9
// Paste: Cmd + V
if let vDown = CGEvent(keyboardEventSource: src, virtualKey: kVK_ANSI_V, keyDown: true) {
vDown.flags = .maskCommand
vDown.post(tap: .cghidEventTap)
vDown.post(tap: .cgSessionEventTap)
}
usleep(12000)
if let vUp = CGEvent(keyboardEventSource: src, virtualKey: kVK_ANSI_V, keyDown: false) {
vUp.flags = .maskCommand
vUp.post(tap: .cghidEventTap)
vUp.post(tap: .cgSessionEventTap)
}
usleep(10000)
return true
}
/**
* Replaces the entire content of active input box cleanly in real-time.
* If text is empty: Cmd+A -> Backspace.
* If text is non-empty: Cmd+A -> Cmd+V.
*/
private func executeCleanFullReplace(_ text: String) -> Bool {
let src = CGEventSource(stateID: .hidSystemState)
let kVK_ANSI_A: CGKeyCode = 0
let kVK_ANSI_V: CGKeyCode = 9
let kVK_Delete: CGKeyCode = 51
if text.isEmpty {
// Select all: Cmd + A
if let aDown = CGEvent(keyboardEventSource: src, virtualKey: kVK_ANSI_A, keyDown: true) {
aDown.flags = .maskCommand
aDown.post(tap: .cghidEventTap)
aDown.post(tap: .cgSessionEventTap)
}
usleep(8000)
if let aUp = CGEvent(keyboardEventSource: src, virtualKey: kVK_ANSI_A, keyDown: false) {
aUp.flags = .maskCommand
aUp.post(tap: .cghidEventTap)
aUp.post(tap: .cgSessionEventTap)
}
usleep(15000)
// Backspace to clear
if let delDown = CGEvent(keyboardEventSource: src, virtualKey: kVK_Delete, keyDown: true) {
delDown.post(tap: .cghidEventTap)
delDown.post(tap: .cgSessionEventTap)
}
usleep(8000)
if let delUp = CGEvent(keyboardEventSource: src, virtualKey: kVK_Delete, keyDown: false) {
delUp.post(tap: .cghidEventTap)
delUp.post(tap: .cgSessionEventTap)
}
return true
}
// Set clipboard
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
pasteboard.setString(text, forType: .string)
// 1. Select all: Cmd + A
if let aDown = CGEvent(keyboardEventSource: src, virtualKey: kVK_ANSI_A, keyDown: true) {
aDown.flags = .maskCommand
aDown.post(tap: .cghidEventTap)
aDown.post(tap: .cgSessionEventTap)
}
usleep(8000)
if let aUp = CGEvent(keyboardEventSource: src, virtualKey: kVK_ANSI_A, keyDown: false) {
aUp.flags = .maskCommand
aUp.post(tap: .cghidEventTap)
aUp.post(tap: .cgSessionEventTap)
}
usleep(18000) // 18ms for selection to settle
// 2. Paste: Cmd + V
if let vDown = CGEvent(keyboardEventSource: src, virtualKey: kVK_ANSI_V, keyDown: true) {
vDown.flags = .maskCommand
vDown.post(tap: .cghidEventTap)
vDown.post(tap: .cgSessionEventTap)
}
usleep(12000)
if let vUp = CGEvent(keyboardEventSource: src, virtualKey: kVK_ANSI_V, keyDown: false) {
vUp.flags = .maskCommand
vUp.post(tap: .cghidEventTap)
vUp.post(tap: .cgSessionEventTap)
}
return true
}
}