Browse Source

fix: restore bulletproof Cmd+V auto-paste mechanism and eliminate HUD halo clipping with expanded canvas and Apple Intelligence specular lighting

main
Ali Alavi 2 weeks ago
parent
commit
8693416bd6
  1. 51
      src/AppDelegate.swift
  2. 208
      src/HUDOverlay.swift
  3. 203
      src/InputBoxManager.swift

51
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()
}

208
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()
}
}

203
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)
}
}
}
Loading…
Cancel
Save