From 8693416bd6d7108311b034878ab6141f012feb49 Mon Sep 17 00:00:00 2001 From: Ali Alavi Date: Fri, 25 Sep 2026 19:47:26 +0000 Subject: [PATCH] fix: restore bulletproof Cmd+V auto-paste mechanism and eliminate HUD halo clipping with expanded canvas and Apple Intelligence specular lighting --- src/AppDelegate.swift | 51 +++++++--- src/HUDOverlay.swift | 208 +++++++++++++++++++++++--------------- src/InputBoxManager.swift | 203 ++----------------------------------- 3 files changed, 172 insertions(+), 290 deletions(-) diff --git a/src/AppDelegate.swift b/src/AppDelegate.swift index 3c2a28e..4f4037f 100644 --- a/src/AppDelegate.swift +++ b/src/AppDelegate.swift @@ -112,14 +112,14 @@ public final class AppDelegate: NSObject, NSApplicationDelegate { isNetworkDegraded = false lastRecordedAudio.removeAll() - // 1. Prepare target input box (never steals focus if already focused) + // 1. Ensure target input box is focused without stealing user cursor InputBoxManager.shared.ensureFocusedInputBox() // 2. Acquire pre-warmed / fast-connected WebSocket session let session = SonioxSessionPool.shared.acquireSession() self.activeSession = session - // Handle network degradation / packet loss warning + // Handle network degradation warning session.onNetworkDegraded = { [weak self] degraded in guard let self = self else { return } self.isNetworkDegraded = degraded @@ -128,15 +128,6 @@ public final class AppDelegate: NSObject, NSApplicationDelegate { } } - // Handle streaming delta text: insert directly into input box as words are recognized - session.onCommittedDelta = { delta in - // If it's an executive slash command, do NOT type it into the input box - if SonioxSettings.shared.voiceCommands && VoiceCommandExecutor.shared.isSlashCommand(text: delta) { - return - } - InputBoxManager.shared.streamDelta(text: delta) - } - session.onFinalResult = { [weak self] result in DispatchQueue.main.async { guard let self = self else { return } @@ -194,7 +185,6 @@ public final class AppDelegate: NSObject, NSApplicationDelegate { } // Check if smart audio recovery is needed: - // If text is empty or we experienced network failure, and we captured substantial audio (> 0.8s) let needsRetry = (rawText.isEmpty || self.isNetworkDegraded) && self.lastRecordedAudio.count > 25600 if needsRetry { @@ -237,18 +227,51 @@ public final class AppDelegate: NSObject, NSApplicationDelegate { return // STRICT: Slash commands are executed, never pasted! } - // 2. Normal dictation processing & insertion + // 2. Normal dictation processing & paste let processed = TextProcessor.shared.process(text: text) if !processed.isEmpty { VoiceCommandLogger.shared.logDictation(rawSpeech: text, processed: processed) HistoryManager.shared.addEntry(processed) - InputBoxManager.shared.commitFinalText(processedText: processed, addTrailingSpace: SonioxSettings.shared.addTrailingSpace) + self.pasteTextToFrontmostApp(text: processed) HUDOverlayController.shared.show(state: .success) } else { HUDOverlayController.shared.hide() } } + private func pasteTextToFrontmostApp(text: String) { + guard !text.isEmpty else { return } + + // 1. Put into system pasteboard + let textToPaste = SonioxSettings.shared.addTrailingSpace ? (text + " ") : text + let pb = NSPasteboard.general + pb.clearContents() + pb.setString(textToPaste, forType: .string) + + // 2. Check Accessibility permission + if !AXIsProcessTrusted() { + print("Accessibility permission NOT granted for auto-paste!") + let options = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] as CFDictionary + AXIsProcessTrustedWithOptions(options) + return + } + + // 3. Instant paste via CGEvent (Cmd+V) after 40ms to let physical key-release clear + DispatchQueue.main.asyncAfter(deadline: .now() + 0.04) { + let source = CGEventSource(stateID: .combinedSessionState) + let vKeyCode: CGKeyCode = 0x09 // 'v' key + + if let keyDown = CGEvent(keyboardEventSource: source, virtualKey: vKeyCode, keyDown: true), + let keyUp = CGEvent(keyboardEventSource: source, virtualKey: vKeyCode, keyDown: false) { + keyDown.flags = .maskCommand + keyUp.flags = .maskCommand + keyDown.post(tap: .cghidEventTap) + keyUp.post(tap: .cghidEventTap) + print("AppDelegate: Pasted \(textToPaste.count) characters via Cmd+V successfully.") + } + } + } + public func applicationWillTerminate(_ notification: Notification) { HotkeyManager.shared.unregisterHotkeys() } diff --git a/src/HUDOverlay.swift b/src/HUDOverlay.swift index 75c378d..c08d883 100644 --- a/src/HUDOverlay.swift +++ b/src/HUDOverlay.swift @@ -17,6 +17,9 @@ public final class HUDOverlayController { private var orbView: SiriOrbView? private var isVisible = false + // Large canvas so shadows/halos NEVER clip at window boundaries + private let canvasSize: CGFloat = 260 + private init() { DispatchQueue.main.async { self.setupWindow() @@ -24,9 +27,8 @@ public final class HUDOverlayController { } private func setupWindow() { - let size: CGFloat = 84 let panel = NSPanel( - contentRect: NSRect(x: 0, y: 0, width: size, height: size), + contentRect: NSRect(x: 0, y: 0, width: canvasSize, height: canvasSize), styleMask: [.borderless, .nonactivatingPanel], backing: .buffered, defer: false @@ -42,8 +44,10 @@ public final class HUDOverlayController { let container = NSView(frame: panel.contentView!.bounds) container.wantsLayer = true - // Pure Apple Siri-like Circular Orb (Centered, no bottom text) - let orb = SiriOrbView(frame: NSRect(x: 12, y: 12, width: 60, height: 60)) + // Siri-like Circular Orb centered with plenty of breathing room for glowing halos + let orbSize: CGFloat = 64 + let orbOrigin = (canvasSize - orbSize) / 2.0 + let orb = SiriOrbView(frame: NSRect(x: orbOrigin, y: orbOrigin, width: orbSize, height: orbSize)) container.addSubview(orb) self.orbView = orb @@ -55,8 +59,9 @@ public final class HUDOverlayController { private func reposition() { guard let window = self.window, let screen = NSScreen.main else { return } let screenRect = screen.visibleFrame - let x = screenRect.midX - (window.frame.width / 2) - let y = screenRect.minY + 36 + // Center horizontally, position near bottom of screen + let x = screenRect.midX - (canvasSize / 2.0) + let y = screenRect.minY + 12 window.setFrameOrigin(NSPoint(x: x, y: y)) } @@ -100,7 +105,7 @@ public final class HUDOverlayController { case .error: orb.showErrorState() - DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.75) { self.hide() } } @@ -111,7 +116,7 @@ public final class HUDOverlayController { DispatchQueue.main.async { [weak self] in guard let self = self, let window = self.window, self.isVisible else { return } NSAnimationContext.runAnimationGroup({ context in - context.duration = 0.22 + context.duration = 0.20 window.animator().alphaValue = 0.0 }, completionHandler: { window.orderOut(nil) @@ -122,13 +127,15 @@ public final class HUDOverlayController { } } -// MARK: - Dynamic Siri Gradient Orb View with Expanded Audio Reactivity +// MARK: - Premium Apple Intelligence / Siri Multilayer Glowing Orb final class SiriOrbView: NSView { - private let gradientLayer = CAGradientLayer() - private let glowLayer = CALayer() - private let backdropLayer = CALayer() - private var baseRadius: CGFloat = 30 + private let ambientAuraLayer = CALayer() + private let haloLayer = CALayer() + private let acousticRippleLayer = CALayer() + private let glassBackdrop = CALayer() + private let siriGradientLayer = CAGradientLayer() + private let specularHighlight = CAGradientLayer() override init(frame frameRect: NSRect) { super.init(frame: frameRect) @@ -147,39 +154,69 @@ final class SiriOrbView: NSView { layer.masksToBounds = false let bounds = self.bounds - baseRadius = bounds.width / 2.0 + let radius = bounds.width / 2.0 + + // 1. Deep Ambient Soft Aura (Diffused outer glow) + ambientAuraLayer.frame = bounds.insetBy(dx: -32, dy: -32) + ambientAuraLayer.cornerRadius = (bounds.width + 64) / 2.0 + ambientAuraLayer.backgroundColor = NSColor(red: 0.0, green: 0.65, blue: 1.0, alpha: 0.22).cgColor + ambientAuraLayer.shadowColor = NSColor(red: 0.55, green: 0.0, blue: 1.0, alpha: 0.85).cgColor + ambientAuraLayer.shadowRadius = 38 + ambientAuraLayer.shadowOpacity = 0.95 + ambientAuraLayer.shadowOffset = .zero + layer.addSublayer(ambientAuraLayer) + + // 2. Focused Neon Halo + haloLayer.frame = bounds.insetBy(dx: -14, dy: -14) + haloLayer.cornerRadius = (bounds.width + 28) / 2.0 + haloLayer.backgroundColor = NSColor(red: 0.1, green: 0.8, blue: 1.0, alpha: 0.40).cgColor + haloLayer.shadowColor = NSColor(red: 0.0, green: 0.9, blue: 1.0, alpha: 0.85).cgColor + haloLayer.shadowRadius = 18 + haloLayer.shadowOpacity = 0.9 + haloLayer.shadowOffset = .zero + layer.addSublayer(haloLayer) - // 1. Expanded Halo Outer Glow - glowLayer.frame = bounds.insetBy(dx: -16, dy: -16) - glowLayer.cornerRadius = (bounds.width + 32) / 2.0 - glowLayer.backgroundColor = NSColor(red: 0.0, green: 0.75, blue: 1.0, alpha: 0.45).cgColor - glowLayer.shadowColor = NSColor(red: 0.5, green: 0.0, blue: 1.0, alpha: 0.85).cgColor - glowLayer.shadowRadius = 18 - glowLayer.shadowOpacity = 0.9 - glowLayer.shadowOffset = .zero - layer.addSublayer(glowLayer) + // 3. Acoustic Ripple Ring + acousticRippleLayer.frame = bounds.insetBy(dx: -4, dy: -4) + acousticRippleLayer.cornerRadius = (bounds.width + 8) / 2.0 + acousticRippleLayer.borderColor = NSColor(red: 0.0, green: 0.95, blue: 1.0, alpha: 0.35).cgColor + acousticRippleLayer.borderWidth = 1.5 + layer.addSublayer(acousticRippleLayer) - // 2. Dark Glass Backdrop - backdropLayer.frame = bounds - backdropLayer.cornerRadius = baseRadius - backdropLayer.backgroundColor = NSColor.black.withAlphaComponent(0.70).cgColor - backdropLayer.borderColor = NSColor.white.withAlphaComponent(0.25).cgColor - backdropLayer.borderWidth = 1.0 - layer.addSublayer(backdropLayer) + // 4. Dark Glass Spherical Backdrop + glassBackdrop.frame = bounds + glassBackdrop.cornerRadius = radius + glassBackdrop.backgroundColor = NSColor.black.withAlphaComponent(0.85).cgColor + glassBackdrop.borderColor = NSColor.white.withAlphaComponent(0.35).cgColor + glassBackdrop.borderWidth = 1.0 + layer.addSublayer(glassBackdrop) - // 3. Siri Multicolored Gradient Core - gradientLayer.frame = bounds.insetBy(dx: 4, dy: 4) - gradientLayer.cornerRadius = (bounds.width - 8) / 2.0 - gradientLayer.colors = [ - NSColor(red: 0.0, green: 0.90, blue: 1.0, alpha: 0.95).cgColor, // Electric Cyan - NSColor(red: 0.50, green: 0.15, blue: 1.0, alpha: 0.95).cgColor, // Royal Violet - NSColor(red: 1.0, green: 0.05, blue: 0.65, alpha: 0.95).cgColor // Hot Magenta + // 5. Multi-Color Liquid Siri Gradient Core + siriGradientLayer.frame = bounds.insetBy(dx: 3, dy: 3) + siriGradientLayer.cornerRadius = (bounds.width - 6) / 2.0 + siriGradientLayer.type = .radial + siriGradientLayer.colors = [ + NSColor(red: 0.0, green: 0.95, blue: 1.0, alpha: 1.0).cgColor, // Electric Cyan Center + NSColor(red: 0.50, green: 0.12, blue: 1.0, alpha: 0.95).cgColor, // Royal Violet + NSColor(red: 0.98, green: 0.08, blue: 0.65, alpha: 0.90).cgColor, // Hot Magenta + NSColor(red: 0.10, green: 0.20, blue: 0.95, alpha: 0.85).cgColor // Deep Blue ] - gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0) - gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0) - layer.addSublayer(gradientLayer) + siriGradientLayer.startPoint = CGPoint(x: 0.35, y: 0.35) + siriGradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0) + layer.addSublayer(siriGradientLayer) - // Continuous smooth rotation + // 6. 3D Curved Specular Glass Crescent Highlight (Apple Intelligence style) + specularHighlight.frame = NSRect(x: bounds.midX - 18, y: bounds.maxY - 18, width: 36, height: 12) + specularHighlight.cornerRadius = 6 + specularHighlight.colors = [ + NSColor.white.withAlphaComponent(0.65).cgColor, + NSColor.white.withAlphaComponent(0.0).cgColor + ] + specularHighlight.startPoint = CGPoint(x: 0.5, y: 1.0) + specularHighlight.endPoint = CGPoint(x: 0.5, y: 0.0) + layer.addSublayer(specularHighlight) + + // Start fluid rotation startOrbRotation() } @@ -187,10 +224,10 @@ final class SiriOrbView: NSView { let rotate = CABasicAnimation(keyPath: "transform.rotation.z") rotate.fromValue = 0.0 rotate.toValue = 2 * Double.pi - rotate.duration = 3.5 + rotate.duration = 4.0 rotate.repeatCount = .infinity rotate.isRemovedOnCompletion = false - gradientLayer.add(rotate, forKey: "siriRotate") + siriGradientLayer.add(rotate, forKey: "siriFluidRotate") } func updateAudioLevel(_ level: Float, isLagging: Bool) { @@ -201,32 +238,40 @@ final class SiriOrbView: NSView { if isLagging { // Warm amber pulse for weak internet - gradientLayer.colors = [ - NSColor(red: 1.0, green: 0.65, blue: 0.0, alpha: 0.95).cgColor, - NSColor(red: 1.0, green: 0.30, blue: 0.0, alpha: 0.95).cgColor, - NSColor(red: 0.95, green: 0.85, blue: 0.1, alpha: 0.9).cgColor + siriGradientLayer.colors = [ + NSColor(red: 1.0, green: 0.85, blue: 0.2, alpha: 1.0).cgColor, + NSColor(red: 1.0, green: 0.55, blue: 0.0, alpha: 0.95).cgColor, + NSColor(red: 0.95, green: 0.25, blue: 0.0, alpha: 0.90).cgColor ] - glowLayer.backgroundColor = NSColor(red: 1.0, green: 0.5, blue: 0.0, alpha: 0.5).cgColor - glowLayer.shadowColor = NSColor(red: 1.0, green: 0.4, blue: 0.0, alpha: 0.85).cgColor + haloLayer.backgroundColor = NSColor(red: 1.0, green: 0.5, blue: 0.0, alpha: 0.5).cgColor + haloLayer.shadowColor = NSColor(red: 1.0, green: 0.4, blue: 0.0, alpha: 0.9).cgColor + ambientAuraLayer.shadowColor = NSColor(red: 1.0, green: 0.3, blue: 0.0, alpha: 0.7).cgColor } else { - // Standard vibrant Siri gradient - gradientLayer.colors = [ - NSColor(red: 0.0, green: 0.90, blue: 1.0, alpha: 0.95).cgColor, - NSColor(red: 0.50, green: 0.15, blue: 1.0, alpha: 0.95).cgColor, - NSColor(red: 1.0, green: 0.05, blue: 0.65, alpha: 0.95).cgColor + // High-fidelity Siri electric gradient + siriGradientLayer.colors = [ + NSColor(red: 0.0, green: 0.95, blue: 1.0, alpha: 1.0).cgColor, + NSColor(red: 0.50, green: 0.12, blue: 1.0, alpha: 0.95).cgColor, + NSColor(red: 0.98, green: 0.08, blue: 0.65, alpha: 0.90).cgColor, + NSColor(red: 0.10, green: 0.20, blue: 0.95, alpha: 0.85).cgColor ] - glowLayer.backgroundColor = NSColor(red: 0.0, green: 0.75, blue: 1.0, alpha: 0.45).cgColor - glowLayer.shadowColor = NSColor(red: 0.5, green: 0.0, blue: 1.0, alpha: 0.85).cgColor + haloLayer.backgroundColor = NSColor(red: 0.1, green: 0.8, blue: 1.0, alpha: 0.40).cgColor + haloLayer.shadowColor = NSColor(red: 0.0, green: 0.9, blue: 1.0, alpha: 0.85).cgColor + ambientAuraLayer.shadowColor = NSColor(red: 0.55, green: 0.0, blue: 1.0, alpha: 0.85).cgColor } - // High-sensitivity Audio Reactive Dynamics (Significantly boosted impact) - // Scale expands from 1.0 up to 1.70x with microphone volume - let scale = 1.0 + (clamped * 0.70) + // Massive Audio Dynamic Scaling (Orb expands up to 1.65x with voice) + let scale = 1.0 + (clamped * 0.65) self.layer?.sublayerTransform = CATransform3DMakeScale(scale, scale, 1.0) - // Halo shadow radius and intensity expand dramatically - glowLayer.shadowRadius = 18 + (clamped * 36) - glowLayer.shadowOpacity = Float(0.70 + (clamped * 0.30)) + // Dynamic Halo bloom (Expands smoothly up to 55px radius) + haloLayer.shadowRadius = 18 + (clamped * 36) + haloLayer.shadowOpacity = Float(0.75 + (clamped * 0.25)) + ambientAuraLayer.shadowRadius = 38 + (clamped * 42) + + // Acoustic ripple expands outward + let rippleScale = 1.0 + (clamped * 0.35) + acousticRippleLayer.transform = CATransform3DMakeScale(rippleScale, rippleScale, 1.0) + acousticRippleLayer.opacity = Float(0.3 + (clamped * 0.7)) CATransaction.commit() } @@ -235,20 +280,20 @@ final class SiriOrbView: NSView { CATransaction.begin() CATransaction.setDisableActions(false) CATransaction.setAnimationDuration(0.3) - gradientLayer.colors = [ - NSColor(red: 1.0, green: 0.60, blue: 0.0, alpha: 0.95).cgColor, - NSColor(red: 1.0, green: 0.25, blue: 0.0, alpha: 0.95).cgColor, - NSColor(red: 0.95, green: 0.85, blue: 0.2, alpha: 0.9).cgColor + siriGradientLayer.colors = [ + NSColor(red: 1.0, green: 0.85, blue: 0.2, alpha: 1.0).cgColor, + NSColor(red: 1.0, green: 0.50, blue: 0.0, alpha: 0.95).cgColor, + NSColor(red: 0.95, green: 0.20, blue: 0.0, alpha: 0.90).cgColor ] - glowLayer.shadowColor = NSColor.orange.cgColor - glowLayer.shadowRadius = 28 + haloLayer.shadowColor = NSColor.orange.cgColor + ambientAuraLayer.shadowColor = NSColor.orange.cgColor CATransaction.commit() } func showTranscribingState() { CATransaction.begin() CATransaction.setDisableActions(true) - self.layer?.sublayerTransform = CATransform3DMakeScale(1.05, 1.05, 1.0) + self.layer?.sublayerTransform = CATransform3DMakeScale(1.08, 1.08, 1.0) CATransaction.commit() } @@ -256,12 +301,12 @@ final class SiriOrbView: NSView { CATransaction.begin() CATransaction.setDisableActions(false) CATransaction.setAnimationDuration(0.2) - gradientLayer.colors = [ - NSColor(red: 0.1, green: 0.95, blue: 0.45, alpha: 0.95).cgColor, - NSColor(red: 0.0, green: 0.80, blue: 0.90, alpha: 0.95).cgColor + siriGradientLayer.colors = [ + NSColor(red: 0.2, green: 1.0, blue: 0.5, alpha: 1.0).cgColor, + NSColor(red: 0.0, green: 0.85, blue: 0.8, alpha: 0.95).cgColor ] - glowLayer.shadowColor = NSColor.green.cgColor - glowLayer.shadowRadius = 24 + haloLayer.shadowColor = NSColor.green.cgColor + ambientAuraLayer.shadowColor = NSColor.green.cgColor CATransaction.commit() } @@ -269,12 +314,12 @@ final class SiriOrbView: NSView { CATransaction.begin() CATransaction.setDisableActions(false) CATransaction.setAnimationDuration(0.2) - gradientLayer.colors = [ - NSColor(red: 1.0, green: 0.2, blue: 0.2, alpha: 0.95).cgColor, - NSColor(red: 0.8, green: 0.0, blue: 0.3, alpha: 0.95).cgColor + siriGradientLayer.colors = [ + NSColor(red: 1.0, green: 0.2, blue: 0.2, alpha: 1.0).cgColor, + NSColor(red: 0.8, green: 0.0, blue: 0.4, alpha: 0.95).cgColor ] - glowLayer.shadowColor = NSColor.red.cgColor - glowLayer.shadowRadius = 24 + haloLayer.shadowColor = NSColor.red.cgColor + ambientAuraLayer.shadowColor = NSColor.red.cgColor CATransaction.commit() } @@ -282,7 +327,10 @@ final class SiriOrbView: NSView { CATransaction.begin() CATransaction.setDisableActions(true) self.layer?.sublayerTransform = CATransform3DIdentity - glowLayer.shadowRadius = 18 + haloLayer.shadowRadius = 18 + ambientAuraLayer.shadowRadius = 38 + acousticRippleLayer.transform = CATransform3DIdentity + acousticRippleLayer.opacity = 0.3 CATransaction.commit() } } diff --git a/src/InputBoxManager.swift b/src/InputBoxManager.swift index d0c1530..605d328 100644 --- a/src/InputBoxManager.swift +++ b/src/InputBoxManager.swift @@ -4,21 +4,11 @@ import ApplicationServices public final class InputBoxManager { public static let shared = InputBoxManager() - private var targetedElement: AXUIElement? - private var streamedText: String = "" - private let lock = NSLock() - private init() {} /// Prepares and guarantees that an input box is focused before or at the start of recording. /// STRICT RULE: If an input box is already focused, NEVER click or steal focus! public func ensureFocusedInputBox() { - lock.lock() - defer { lock.unlock() } - - streamedText = "" - targetedElement = nil - guard AXIsProcessTrusted() else { print("InputBoxManager: Accessibility not trusted.") return @@ -30,182 +20,30 @@ public final class InputBoxManager { // 1. Check if an element is already focused in the frontmost application var focusedObj: CFTypeRef? if AXUIElementCopyAttributeValue(appElem, kAXFocusedUIElementAttribute as CFString, &focusedObj) == .success, - let focused = focusedObj { - let elem = (focused as! AXUIElement) - if isEditableElement(elem) { - print("InputBoxManager: User already has focused editable element in \(frontApp.localizedName ?? ""). Preserving focus.") - targetedElement = elem - return - } + focusedObj != nil { + // Already focused, preserve user focus untouched! + return } // 2. Check systemWide focus let sysWide = AXUIElementCreateSystemWide() if AXUIElementCopyAttributeValue(sysWide, kAXFocusedUIElementAttribute as CFString, &focusedObj) == .success, - let focused = focusedObj { - let elem = (focused as! AXUIElement) - if isEditableElement(elem) { - print("InputBoxManager: System-wide focused editable element found. Preserving focus.") - targetedElement = elem - return - } + focusedObj != nil { + return } - // 3. Only if NOTHING is focused, search active window and gently focus the best input field + // 3. Only if NOTHING is focused, search active window and gently set focus var windowObj: CFTypeRef? if AXUIElementCopyAttributeValue(appElem, kAXFocusedWindowAttribute as CFString, &windowObj) == .success, let win = windowObj { let winElem = (win as! AXUIElement) if let bestInput = findBestInputElement(in: winElem) { - print("InputBoxManager: No focus detected. Auto-focusing best input field.") + print("InputBoxManager: No active focus detected. Gently focusing target input field in \(frontApp.localizedName ?? "").") AXUIElementSetAttributeValue(bestInput, kAXFocusedAttribute as CFString, kCFBooleanTrue) - if let center = getElementCenter(bestInput) { - clickAtPoint(center) - } - targetedElement = bestInput - } - } - } - - /// Stream delta text (incremental words) live into the focused input box as they are recognized - public func streamDelta(text: String) { - guard !text.isEmpty else { return } - - lock.lock() - streamedText += text - let target = targetedElement - lock.unlock() - - // 1. Try Accessibility selected text insertion if supported - if let elem = target { - var selectedTextObj: CFTypeRef? - if AXUIElementCopyAttributeValue(elem, kAXSelectedTextAttribute as CFString, &selectedTextObj) == .success { - let err = AXUIElementSetAttributeValue(elem, kAXSelectedTextAttribute as CFString, text as CFString) - if err == .success { - return - } - } - } - - // 2. High-speed Unicode typing using clean private state (strips physical Option key modifier!) - typeUnicodeString(text) - } - - /// Commits final text: guarantees 100% insertion into input box AND system clipboard - public func commitFinalText(processedText: String, addTrailingSpace: Bool) { - let trimmed = processedText.trimmingCharacters(in: .whitespacesAndNewlines) - guard !trimmed.isEmpty else { return } - - let textToClipboard = addTrailingSpace ? (trimmed + " ") : trimmed - - // 1. Always copy full processed text to system clipboard - let pb = NSPasteboard.general - pb.clearContents() - pb.setString(textToClipboard, forType: .string) - - lock.lock() - let previouslyStreamed = streamedText - streamedText = "" - targetedElement = nil - lock.unlock() - - let cleanStreamed = previouslyStreamed.trimmingCharacters(in: .whitespacesAndNewlines) - - // Case A: Nothing was streamed live (e.g. short utterance, lag, or burst retry) -> PASTE FULL TEXT - if cleanStreamed.isEmpty { - print("InputBoxManager: No live stream occurred. Pasting complete text via clipboard...") - pasteClipboardToFrontmostApp() - return - } - - // Case B: Words were streamed live. Check if there is trailing text not yet typed. - if trimmed.count > cleanStreamed.count { - // Find unstreamed suffix - if trimmed.hasPrefix(cleanStreamed) { - let suffix = String(trimmed.dropFirst(cleanStreamed.count)) - let suffixToType = addTrailingSpace ? (suffix + " ") : suffix - print("InputBoxManager: Streaming remaining trailing suffix: '\(suffixToType)'") - typeUnicodeString(suffixToType) - } else { - // TextProcessor modified words. Add space or trailing tail - if addTrailingSpace { - typeUnicodeString(" ") - } - } - } else if addTrailingSpace { - typeUnicodeString(" ") - } - } - - /// Simulates Cmd+V cleanly using private CGEvent source (stripping any active Option key) - public func pasteClipboardToFrontmostApp() { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { - let source = CGEventSource(stateID: .privateState) - let vKeyCode: CGKeyCode = 0x09 // 'v' key - - guard let down = CGEvent(keyboardEventSource: source, virtualKey: vKeyCode, keyDown: true), - let up = CGEvent(keyboardEventSource: source, virtualKey: vKeyCode, keyDown: false) else { - return - } - - // Explicitly set only Command flag, stripping Option/Control/Shift - down.flags = .maskCommand - up.flags = .maskCommand - - down.post(tap: .cghidEventTap) - up.post(tap: .cghidEventTap) - } - } - - // MARK: - Core Unicode Typing (Isolated from Physical Modifiers) - - private func typeUnicodeString(_ string: String) { - let source = CGEventSource(stateID: .privateState) - let utf16 = Array(string.utf16) - - // Send in small fast chunks - let chunkSize = 16 - var offset = 0 - while offset < utf16.count { - let count = min(chunkSize, utf16.count - offset) - let sub = Array(utf16[offset..<(offset + count)]) - - guard let keyDown = CGEvent(keyboardEventSource: source, virtualKey: 0, keyDown: true), - let keyUp = CGEvent(keyboardEventSource: source, virtualKey: 0, keyDown: false) else { - return - } - - keyDown.flags = [] - keyUp.flags = [] - keyDown.keyboardSetUnicodeString(stringLength: sub.count, unicodeString: sub) - keyUp.keyboardSetUnicodeString(stringLength: sub.count, unicodeString: sub) - - keyDown.post(tap: .cghidEventTap) - keyUp.post(tap: .cghidEventTap) - - offset += count - if offset < utf16.count { - usleep(4000) // 4ms } } } - // MARK: - Accessibility Helpers - - private func isEditableElement(_ elem: AXUIElement) -> Bool { - var roleObj: CFTypeRef? - AXUIElementCopyAttributeValue(elem, kAXRoleAttribute as CFString, &roleObj) - let role = (roleObj as? String) ?? "" - if role == "AXTextField" || role == "AXTextArea" || role == "AXComboBox" || role == "AXWebArea" { - return true - } - var valObj: CFTypeRef? - if AXUIElementCopyAttributeValue(elem, kAXValueAttribute as CFString, &valObj) == .success { - return true - } - return false - } - private func findBestInputElement(in parent: AXUIElement, maxDepth: Int = 8, depth: Int = 0) -> AXUIElement? { if depth > maxDepth { return nil } @@ -228,31 +66,4 @@ public final class InputBoxManager { } return nil } - - private func getElementCenter(_ elem: AXUIElement) -> CGPoint? { - var posVal: CFTypeRef? - var sizeVal: CFTypeRef? - guard AXUIElementCopyAttributeValue(elem, kAXPositionAttribute as CFString, &posVal) == .success, - AXUIElementCopyAttributeValue(elem, kAXSizeAttribute as CFString, &sizeVal) == .success else { - return nil - } - var pt = CGPoint.zero - var sz = CGSize.zero - guard AXValueGetValue(posVal as! AXValue, .cgPoint, &pt), - AXValueGetValue(sizeVal as! AXValue, .cgSize, &sz) else { - return nil - } - return CGPoint(x: pt.x + sz.width / 2.0, y: pt.y + sz.height / 2.0) - } - - private func clickAtPoint(_ point: CGPoint) { - let source = CGEventSource(stateID: .privateState) - if let clickDown = CGEvent(mouseEventSource: source, mouseType: .leftMouseDown, mouseCursorPosition: point, mouseButton: .left), - let clickUp = CGEvent(mouseEventSource: source, mouseType: .leftMouseUp, mouseCursorPosition: point, mouseButton: .left) { - clickDown.flags = [] - clickUp.flags = [] - clickDown.post(tap: .cghidEventTap) - clickUp.post(tap: .cghidEventTap) - } - } }