|
|
@ -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 { |
|
|
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) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
// Full Cmd + A sequence |
|
|
|
|
|
let cmdDown = CGEvent(keyboardEventSource: src, virtualKey: kVK_Command, keyDown: true) |
|
|
|
|
|
cmdDown?.flags = .maskCommand |
|
|
|
|
|
cmdDown?.post(tap: .cghidEventTap) |
|
|
|
|
|
usleep(8000) |
|
|
|
|
|
|
|
|
|
|
|
let aDown = CGEvent(keyboardEventSource: src, virtualKey: kVK_ANSI_A, keyDown: true) |
|
|
|
|
|
aDown?.flags = .maskCommand |
|
|
|
|
|
aDown?.post(tap: .cghidEventTap) |
|
|
|
|
|
usleep(12000) |
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 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) |
|
|
usleep(15000) |
|
|
if let aUp = CGEvent(keyboardEventSource: src, virtualKey: 0, keyDown: false) { |
|
|
|
|
|
aUp.flags = .maskCommand |
|
|
|
|
|
aUp.post(tap: .cghidEventTap) |
|
|
|
|
|
} |
|
|
|
|
|
usleep(40000) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 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 vUp = CGEvent(keyboardEventSource: src, virtualKey: kVK_ANSI_V, keyDown: false) |
|
|
|
|
|
vUp?.flags = .maskCommand |
|
|
|
|
|
vUp?.post(tap: .cghidEventTap) |
|
|
|
|
|
usleep(8000) |
|
|
|
|
|
|
|
|
// 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) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
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 |
|
|
} |
|
|
} |
|
|
} |
|
|
} |