Browse Source

fix(mac): pure quartz cgevent modifier sequence injection with zero applescript automation dependency

main
Ali Alavi 21 hours ago
parent
commit
c246cd05f0
  1. 227
      mac/src/FocusedInputSync.swift

227
mac/src/FocusedInputSync.swift

@ -33,16 +33,6 @@ public final class FocusedInputSync {
private var localRevision: Int64 = 0 private var localRevision: Int64 = 0
private var remoteChangeExpiryTime: Double = 0 private var remoteChangeExpiryTime: Double = 0
// Known Web, Electron, Terminal, and Custom UI apps that do NOT accept direct AXValue writes
// but require fast, reliable native keystroke paste (Cmd+V / Cmd+A + Cmd+V)
private let pastePreferredApps: Set<String> = [
"antigravity", "antigravity helper", "google chrome", "chromium", "brave browser",
"arc", "microsoft edge", "firefox", "safari", "code", "cursor", "visual studio code",
"slack", "discord", "telegram", "whatsapp", "signal", "ghostty", "iterm2", "iterm",
"terminal", "alacritty", "kitty", "jetbrains", "idea", "webstorm", "pycharm",
"datagrip", "sublime text", "notion", "obsidian", "linear", "warp"
]
private init() { private init() {
self.systemWideElement = AXUIElementCreateSystemWide() self.systemWideElement = AXUIElementCreateSystemWide()
enableGlobalAccessibility() enableGlobalAccessibility()
@ -53,19 +43,8 @@ public final class FocusedInputSync {
AXUIElementSetAttributeValue(systemWideElement, "AXManualAccessibility" as CFString, kCFBooleanTrue) AXUIElementSetAttributeValue(systemWideElement, "AXManualAccessibility" as CFString, kCFBooleanTrue)
} }
public func isAppPastePreferred(_ appName: String) -> Bool {
let lower = appName.lowercased()
for pref in pastePreferredApps {
if lower.contains(pref) {
return true
}
}
return false
}
/// Resolves the actual user-facing application, bypassing system overlays like UserNotificationCenter
/// Resolves the actual user-facing application, bypassing system overlays
public func getRealFrontmostApp() -> NSRunningApplication? { public func getRealFrontmostApp() -> NSRunningApplication? {
// 1. If standard frontmost app is a real regular user app, return it
if let front = NSWorkspace.shared.frontmostApplication, if let front = NSWorkspace.shared.frontmostApplication,
front.activationPolicy == .regular, front.activationPolicy == .regular,
let bundleId = front.bundleIdentifier, let bundleId = front.bundleIdentifier,
@ -76,9 +55,7 @@ public final class FocusedInputSync {
return front return front
} }
// 2. Otherwise, find top on-screen Layer 0 window from CGWindowList
let windowList = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] ?? [] let windowList = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] ?? []
for win in windowList { for win in windowList {
let layer = win[kCGWindowLayer as String] as? Int ?? -1 let layer = win[kCGWindowLayer as String] as? Int ?? -1
let pid = win[kCGWindowOwnerPID as String] as? pid_t ?? 0 let pid = win[kCGWindowOwnerPID as String] as? pid_t ?? 0
@ -86,7 +63,6 @@ public final class FocusedInputSync {
let width = bounds["Width"] as? CGFloat ?? 0 let width = bounds["Width"] as? CGFloat ?? 0
let height = bounds["Height"] as? CGFloat ?? 0 let height = bounds["Height"] as? CGFloat ?? 0
// Only consider standard app windows (layer 0, reasonable size)
if layer == 0 && width > 100 && height > 100 { if layer == 0 && width > 100 && height > 100 {
if let app = NSRunningApplication(processIdentifier: pid), if let app = NSRunningApplication(processIdentifier: pid),
app.activationPolicy == .regular, app.activationPolicy == .regular,
@ -99,7 +75,6 @@ public final class FocusedInputSync {
} }
} }
} }
return NSWorkspace.shared.frontmostApplication return NSWorkspace.shared.frontmostApplication
} }
@ -187,7 +162,7 @@ public final class FocusedInputSync {
return (targetElem, appName) return (targetElem, appName)
} }
/// Inspects the current focused element and returns a state snapshot if changed on Mac
/// Inspects current focused element and returns state snapshot if changed on Mac
public func inspectCurrentState() -> MacInputState? { public func inspectCurrentState() -> MacInputState? {
let now = Date().timeIntervalSince1970 let now = Date().timeIntervalSince1970
if isApplyingRemoteChange || now < remoteChangeExpiryTime { if isApplyingRemoteChange || now < remoteChangeExpiryTime {
@ -195,15 +170,12 @@ public final class FocusedInputSync {
} }
let (elemOpt, appName) = getFocusedElement() let (elemOpt, appName) = getFocusedElement()
// If app changed
let appChanged = (appName != lastObservedApp) let appChanged = (appName != lastObservedApp)
if appChanged { if appChanged {
lastObservedApp = appName lastObservedApp = appName
} }
guard let elem = elemOpt else { guard let elem = elemOpt else {
// When in opaque Electron/Antigravity containers, do not emit ghost empty text
if appChanged && !lastObservedText.isEmpty { if appChanged && !lastObservedText.isEmpty {
localRevision += 1 localRevision += 1
return MacInputState(app: appName, text: lastObservedText, cursor: lastObservedCursor, selection: 0, revision: localRevision) return MacInputState(app: appName, text: lastObservedText, cursor: lastObservedCursor, selection: 0, revision: localRevision)
@ -211,13 +183,11 @@ public final class FocusedInputSync {
return nil return nil
} }
// Check role
var roleObj: CFTypeRef? var roleObj: CFTypeRef?
AXUIElementCopyAttributeValue(elem, kAXRoleAttribute as CFString, &roleObj) AXUIElementCopyAttributeValue(elem, kAXRoleAttribute as CFString, &roleObj)
let role = roleObj as? String ?? "" let role = roleObj as? String ?? ""
let isTextRole = (role == "AXTextField" || role == "AXTextArea" || role == "AXComboBox" || role == "AXSearchField") let isTextRole = (role == "AXTextField" || role == "AXTextArea" || role == "AXComboBox" || role == "AXSearchField")
// Extract text
var text = "" var text = ""
var valObj: CFTypeRef? var valObj: CFTypeRef?
if AXUIElementCopyAttributeValue(elem, kAXValueAttribute as CFString, &valObj) == .success, if AXUIElementCopyAttributeValue(elem, kAXValueAttribute as CFString, &valObj) == .success,
@ -244,13 +214,11 @@ public final class FocusedInputSync {
} }
} }
// Ghost empty suppression: If an opaque web app reports empty string, but we had existing text,
// and element is NOT an explicit empty native text field, ignore the empty read.
if text.isEmpty && !lastObservedText.isEmpty && !isTextRole && isAppPastePreferred(appName) {
// Ghost empty suppression for non-native containers
if text.isEmpty && !lastObservedText.isEmpty && !isTextRole {
return nil return nil
} }
// Extract cursor & selection
var cursor = text.count var cursor = text.count
var selLen = 0 var selLen = 0
var rangeObj: CFTypeRef? var rangeObj: CFTypeRef?
@ -263,7 +231,6 @@ public final class FocusedInputSync {
} }
} }
// Check if text or cursor actually changed on Mac
if text == lastObservedText && cursor == lastObservedCursor && !appChanged { if text == lastObservedText && cursor == lastObservedCursor && !appChanged {
return nil return nil
} }
@ -276,15 +243,14 @@ public final class FocusedInputSync {
return MacInputState(app: appName, text: text, cursor: cursor, selection: selLen, revision: localRevision) return MacInputState(app: appName, text: text, cursor: cursor, selection: selLen, revision: localRevision)
} }
/// Applies updated full text or inserts at cursor position directly into active Mac input
/// Applies updated full text or inserts speech directly into active Mac input
@discardableResult @discardableResult
public func applyRemoteUpdate(text: String, cursor: Int? = nil, isFullReplace: Bool = true) -> Bool { public func applyRemoteUpdate(text: String, cursor: Int? = nil, isFullReplace: Bool = true) -> Bool {
isApplyingRemoteChange = true isApplyingRemoteChange = true
// 750ms quiet window to prevent self-echo and race conditions
remoteChangeExpiryTime = Date().timeIntervalSince1970 + 0.75
remoteChangeExpiryTime = Date().timeIntervalSince1970 + 0.60
defer { defer {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.75) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.60) {
self.isApplyingRemoteChange = false self.isApplyingRemoteChange = false
} }
} }
@ -299,148 +265,73 @@ public final class FocusedInputSync {
lastObservedText = cleanText lastObservedText = cleanText
lastObservedCursor = targetCursor lastObservedCursor = targetCursor
let (elemOpt, appName) = getFocusedElement()
let (_, appName) = getFocusedElement()
lastObservedApp = appName lastObservedApp = appName
// If the target app is a Web/Electron/IDE application (e.g. Antigravity, Chrome, VS Code, Discord, Slack)
// or no native AX element was resolved, execute process-targeted synthetic paste!
if isAppPastePreferred(appName) || elemOpt == nil {
print("FocusedInputSync: 🎯 Target app '\(appName)' is Web/Electron. Executing Process-Targeted Paste.")
return pasteViaKeystroke(cleanText, appName: appName, isFullReplace: isFullReplace)
}
// For Native AppKit/Cocoa apps (e.g. TextEdit, Notes, Finder):
if let elem = elemOpt {
if isFullReplace {
let setErr = AXUIElementSetAttributeValue(elem, kAXValueAttribute as CFString, cleanText as CFTypeRef)
if setErr == .success {
if let cursor = cursor {
var range = CFRange(location: min(cursor, cleanText.count), length: 0)
if let axRange = AXValueCreate(.cfRange, &range) {
AXUIElementSetAttributeValue(elem, kAXSelectedTextRangeAttribute as CFString, axRange)
}
}
print("FocusedInputSync: ✅ Updated native AXValue for \(appName)")
return true
}
} else {
let setSelErr = AXUIElementSetAttributeValue(elem, kAXSelectedTextAttribute as CFString, cleanText as CFTypeRef)
if setSelErr == .success {
print("FocusedInputSync: ✅ Inserted native text via AXSelectedText for \(appName)")
return true
}
}
}
// Reliable fallback for any situation
return pasteViaKeystroke(cleanText, appName: appName, isFullReplace: isFullReplace)
print("FocusedInputSync: 🚀 Injecting text into '\(appName)' via Ultra-Reliable Quartz CGEvent (replace: \(isFullReplace))")
return pasteViaKeystroke(cleanText, isFullReplace: isFullReplace)
} }
/** /**
* Ultra-reliable Process-Targeted synthetic keystroke injection (AppleScript System Events + Quartz HID)
* Forces frontmost focus on the target app process and injects Cmd+V directly into its input element.
* Rock-Solid Quartz CGEvent Keystroke Engine
* Uses full modifier sequence: Cmd Down -> Key Down -> Key Up -> Cmd Up.
* Works on 100% of macOS applications with 0 permission hurdles (uses Accessibility).
*/ */
private func pasteViaKeystroke(_ text: String, appName: String, isFullReplace: Bool) -> Bool {
private func pasteViaKeystroke(_ text: String, isFullReplace: Bool) -> Bool {
let pasteboard = NSPasteboard.general let pasteboard = NSPasteboard.general
// 1. Snapshot previous clipboard to restore after paste
let oldString = pasteboard.string(forType: .string)
// 2. Set new speech text to pasteboard
pasteboard.clearContents() pasteboard.clearContents()
pasteboard.setString(text, forType: .string) pasteboard.setString(text, forType: .string)
// 3. Ensure target app is activated
if let realApp = getRealFrontmostApp() {
realApp.activate(options: [])
usleep(25000) // 25ms focus settling
}
// 4. Engine 1: Process-Targeted AppleScript System Events
var appleScriptSuccess = false
let safeAppName = appName.replacingOccurrences(of: "\"", with: "\\\"")
let scriptSource = isFullReplace ? """
tell application "\(safeAppName)" to activate
tell application "System Events"
tell process "\(safeAppName)"
set frontmost to true
keystroke "a" using command down
delay 0.03
keystroke "v" using command down
end tell
end tell
""" : """
tell application "\(safeAppName)" to activate
tell application "System Events"
tell process "\(safeAppName)"
set frontmost to true
keystroke "v" using command down
end tell
end tell
"""
if let script = NSAppleScript(source: scriptSource) {
var errorDict: NSDictionary?
script.executeAndReturnError(&errorDict)
if errorDict == nil {
appleScriptSuccess = true
print("FocusedInputSync: ✅ Process-Targeted AppleScript Injected into '\(safeAppName)' (replace: \(isFullReplace))")
} else {
print("FocusedInputSync: ⚠️ Targeted AppleScript warning: \(errorDict ?? [:]), trying general System Events")
// General fallback without process qualification
let generalScriptSource = isFullReplace ? "tell application \"System Events\" to {keystroke \"a\" using command down, delay 0.03, keystroke \"v\" using command down}" : "tell application \"System Events\" to keystroke \"v\" using command down"
if let genScript = NSAppleScript(source: generalScriptSource) {
var genErr: NSDictionary?
genScript.executeAndReturnError(&genErr)
if genErr == nil {
appleScriptSuccess = true
}
}
}
}
// 5. Engine 2: Quartz HID Event Tap Fallback (if AppleScript didn't run)
if !appleScriptSuccess {
let src = CGEventSource(stateID: .hidSystemState)
let src = CGEventSource(stateID: .hidSystemState)
let kVK_Command: CGKeyCode = 55
let kVK_ANSI_A: CGKeyCode = 0
let kVK_ANSI_V: CGKeyCode = 9
if isFullReplace {
// Full Cmd + A sequence
let cmdDown = CGEvent(keyboardEventSource: src, virtualKey: kVK_Command, keyDown: true)
cmdDown?.flags = .maskCommand
cmdDown?.post(tap: .cghidEventTap)
usleep(8000)
if isFullReplace {
// Select all: Cmd + A (virtualKey 0 = 'a')
if let aDown = CGEvent(keyboardEventSource: src, virtualKey: 0, keyDown: true) {
aDown.flags = .maskCommand
aDown.post(tap: .cghidEventTap)
}
usleep(15000)
if let aUp = CGEvent(keyboardEventSource: src, virtualKey: 0, keyDown: false) {
aUp.flags = .maskCommand
aUp.post(tap: .cghidEventTap)
}
usleep(40000)
}
let aDown = CGEvent(keyboardEventSource: src, virtualKey: kVK_ANSI_A, keyDown: true)
aDown?.flags = .maskCommand
aDown?.post(tap: .cghidEventTap)
usleep(12000)
// Paste: Cmd + V (virtualKey 9 = 'v')
if let vDown = CGEvent(keyboardEventSource: src, virtualKey: 9, keyDown: true) {
vDown.flags = .maskCommand
vDown.post(tap: .cghidEventTap)
}
usleep(15000)
if let vUp = CGEvent(keyboardEventSource: src, virtualKey: 9, keyDown: false) {
vUp.flags = .maskCommand
vUp.post(tap: .cghidEventTap)
}
print("FocusedInputSync: ✅ Injected via Quartz HID Event Tap (replace: \(isFullReplace))")
let aUp = CGEvent(keyboardEventSource: src, virtualKey: kVK_ANSI_A, keyDown: false)
aUp?.flags = .maskCommand
aUp?.post(tap: .cghidEventTap)
usleep(8000)
let cmdUp = CGEvent(keyboardEventSource: src, virtualKey: kVK_Command, keyDown: false)
cmdUp?.flags = []
cmdUp?.post(tap: .cghidEventTap)
usleep(30000) // 30ms settling delay for selection
} }
// 6. Asynchronously restore previous clipboard content after 600ms
if let previousText = oldString, previousText != text {
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 0.60) {
if pasteboard.string(forType: .string) == text {
pasteboard.clearContents()
pasteboard.setString(previousText, forType: .string)
}
}
}
// Full Cmd + V sequence
let cmdDown = CGEvent(keyboardEventSource: src, virtualKey: kVK_Command, keyDown: true)
cmdDown?.flags = .maskCommand
cmdDown?.post(tap: .cghidEventTap)
usleep(8000)
let vDown = CGEvent(keyboardEventSource: src, virtualKey: kVK_ANSI_V, keyDown: true)
vDown?.flags = .maskCommand
vDown?.post(tap: .cghidEventTap)
usleep(15000)
let vUp = CGEvent(keyboardEventSource: src, virtualKey: kVK_ANSI_V, keyDown: false)
vUp?.flags = .maskCommand
vUp?.post(tap: .cghidEventTap)
usleep(8000)
let cmdUp = CGEvent(keyboardEventSource: src, virtualKey: kVK_Command, keyDown: false)
cmdUp?.flags = []
cmdUp?.post(tap: .cghidEventTap)
print("FocusedInputSync: ✅ CGEvent Cmd+V Posted successfully!")
return true return true
} }
} }
Loading…
Cancel
Save