Browse Source

feat: word-by-word streaming delta into input boxes via private annotated paste queue, minimal non-rotating Siri orb with voice-reactive stroke breathing

main
Ali Alavi 2 weeks ago
parent
commit
72de20c523
  1. 46
      src/AppDelegate.swift
  2. 250
      src/HUDOverlay.swift
  3. 102
      src/InputBoxManager.swift

46
src/AppDelegate.swift

@ -128,6 +128,15 @@ public final class AppDelegate: NSObject, NSApplicationDelegate {
}
}
// Stream live finalized tokens directly into input box word-by-word
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 }
@ -227,51 +236,18 @@ public final class AppDelegate: NSObject, NSApplicationDelegate {
return // STRICT: Slash commands are executed, never pasted!
}
// 2. Normal dictation processing & paste
// 2. Normal dictation processing & final commit
let processed = TextProcessor.shared.process(text: text)
if !processed.isEmpty {
VoiceCommandLogger.shared.logDictation(rawSpeech: text, processed: processed)
HistoryManager.shared.addEntry(processed)
self.pasteTextToFrontmostApp(text: processed)
InputBoxManager.shared.commitFinalText(fullText: processed, addTrailingSpace: SonioxSettings.shared.addTrailingSpace)
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()
}

250
src/HUDOverlay.swift

@ -14,11 +14,11 @@ public final class HUDOverlayController {
public static let shared = HUDOverlayController()
private var window: NSPanel?
private var orbView: SiriOrbView?
private var orbView: MinimalSiriOrbView?
private var isVisible = false
// Large canvas so shadows/halos NEVER clip at window boundaries
private let canvasSize: CGFloat = 260
// Transparent canvas with plenty of margin so halo never clips
private let canvasSize: CGFloat = 160
private init() {
DispatchQueue.main.async {
@ -44,10 +44,10 @@ public final class HUDOverlayController {
let container = NSView(frame: panel.contentView!.bounds)
container.wantsLayer = true
// 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))
// Minimal Siri Orb centered in canvas
let orbDiameter: CGFloat = 52
let orbOrigin = (canvasSize - orbDiameter) / 2.0
let orb = MinimalSiriOrbView(frame: NSRect(x: orbOrigin, y: orbOrigin, width: orbDiameter, height: orbDiameter))
container.addSubview(orb)
self.orbView = orb
@ -59,9 +59,8 @@ public final class HUDOverlayController {
private func reposition() {
guard let window = self.window, let screen = NSScreen.main else { return }
let screenRect = screen.visibleFrame
// Center horizontally, position near bottom of screen
let x = screenRect.midX - (canvasSize / 2.0)
let y = screenRect.minY + 12
let y = screenRect.minY + 28
window.setFrameOrigin(NSPoint(x: x, y: y))
}
@ -99,13 +98,13 @@ public final class HUDOverlayController {
case .success:
orb.showSuccessState()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.45) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.40) {
self.hide()
}
case .error:
orb.showErrorState()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.75) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.70) {
self.hide()
}
}
@ -116,7 +115,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.20
context.duration = 0.18
window.animator().alphaValue = 0.0
}, completionHandler: {
window.orderOut(nil)
@ -127,15 +126,16 @@ public final class HUDOverlayController {
}
}
// MARK: - Premium Apple Intelligence / Siri Multilayer Glowing Orb
// MARK: - Minimal Clean Siri Orb (Static, Non-Rotating, Audio-Reactive Stroke & Glow)
final class SiriOrbView: NSView {
private let ambientAuraLayer = CALayer()
private let haloLayer = CALayer()
private let acousticRippleLayer = CALayer()
private let glassBackdrop = CALayer()
private let siriGradientLayer = CAGradientLayer()
private let specularHighlight = CAGradientLayer()
final class MinimalSiriOrbView: NSView {
private let glowLayer = CALayer()
private let gradientCoreLayer = CAGradientLayer()
private let strokeBorderLayer = CALayer()
private let baseDiameter: CGFloat = 52.0
private let minStrokeWidth: CGFloat = 1.5
private let maxStrokeWidth: CGFloat = 5.5
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
@ -156,80 +156,41 @@ final class SiriOrbView: NSView {
let bounds = self.bounds
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)
// 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)
// 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)
// 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
]
siriGradientLayer.startPoint = CGPoint(x: 0.35, y: 0.35)
siriGradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
layer.addSublayer(siriGradientLayer)
// 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
// 1. Soft Outer Atmospheric Glow (Breathes with voice volume)
glowLayer.frame = bounds.insetBy(dx: -10, dy: -10)
glowLayer.cornerRadius = (bounds.width + 20) / 2.0
glowLayer.backgroundColor = NSColor(red: 0.05, green: 0.65, blue: 1.0, alpha: 0.35).cgColor
glowLayer.shadowColor = NSColor(red: 0.50, green: 0.10, blue: 0.95, alpha: 0.85).cgColor
glowLayer.shadowRadius = 14
glowLayer.shadowOpacity = 0.60
glowLayer.shadowOffset = .zero
layer.addSublayer(glowLayer)
// 2. Clean, Minimal, Non-Rotating Siri Gradient Sphere
gradientCoreLayer.frame = bounds
gradientCoreLayer.cornerRadius = radius
gradientCoreLayer.type = .radial
gradientCoreLayer.colors = [
NSColor(red: 0.0, green: 0.92, blue: 1.0, alpha: 1.0).cgColor, // Electric Cyan Core
NSColor(red: 0.45, green: 0.12, blue: 0.96, alpha: 0.98).cgColor, // Rich Royal Violet
NSColor(red: 0.90, green: 0.08, blue: 0.58, alpha: 0.92).cgColor, // Deep Magenta
NSColor(red: 0.08, green: 0.10, blue: 0.35, alpha: 0.88).cgColor // Deep Navy Edge
]
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()
}
private func startOrbRotation() {
let rotate = CABasicAnimation(keyPath: "transform.rotation.z")
rotate.fromValue = 0.0
rotate.toValue = 2 * Double.pi
rotate.duration = 4.0
rotate.repeatCount = .infinity
rotate.isRemovedOnCompletion = false
siriGradientLayer.add(rotate, forKey: "siriFluidRotate")
gradientCoreLayer.startPoint = CGPoint(x: 0.40, y: 0.40)
gradientCoreLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
layer.addSublayer(gradientCoreLayer)
// 3. Dynamic Audio-Reactive Stroke Ring (Widening & brightening with speech)
strokeBorderLayer.frame = bounds
strokeBorderLayer.cornerRadius = radius
strokeBorderLayer.borderWidth = minStrokeWidth
strokeBorderLayer.borderColor = NSColor.white.withAlphaComponent(0.35).cgColor
strokeBorderLayer.backgroundColor = NSColor.clear.cgColor
layer.addSublayer(strokeBorderLayer)
}
/// Updates audio response: The orb remains steady (no jiggling/scaling),
/// while the stroke border thickness and outer glow bloom dynamically with voice.
func updateAudioLevel(_ level: Float, isLagging: Bool) {
CATransaction.begin()
CATransaction.setDisableActions(true)
@ -237,41 +198,40 @@ final class SiriOrbView: NSView {
let clamped = CGFloat(min(max(level, 0.0), 1.0))
if isLagging {
// Warm amber pulse for weak internet
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
// Warm amber indicator for network degradation
gradientCoreLayer.colors = [
NSColor(red: 1.0, green: 0.80, blue: 0.20, alpha: 1.0).cgColor,
NSColor(red: 1.0, green: 0.45, blue: 0.0, alpha: 0.95).cgColor,
NSColor(red: 0.85, green: 0.15, blue: 0.0, alpha: 0.90).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
let strokeColor = NSColor(red: 1.0, green: 0.70, blue: 0.20, alpha: 0.6 + (0.4 * clamped))
strokeBorderLayer.borderColor = strokeColor.cgColor
glowLayer.shadowColor = NSColor.orange.cgColor
} else {
// 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
// Standard crisp Siri palette
gradientCoreLayer.colors = [
NSColor(red: 0.0, green: 0.92, blue: 1.0, alpha: 1.0).cgColor,
NSColor(red: 0.45, green: 0.12, blue: 0.96, alpha: 0.98).cgColor,
NSColor(red: 0.90, green: 0.08, blue: 0.58, alpha: 0.92).cgColor,
NSColor(red: 0.08, green: 0.10, blue: 0.35, alpha: 0.88).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
let strokeColor = NSColor(
red: 0.3 + (0.7 * clamped),
green: 0.95,
blue: 1.0,
alpha: 0.35 + (0.65 * clamped)
)
strokeBorderLayer.borderColor = strokeColor.cgColor
glowLayer.shadowColor = NSColor(red: 0.45, green: 0.10, blue: 0.95, alpha: 0.85).cgColor
}
// 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)
// 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)
// 1. Dynamic Stroke Width: from 1.5px up to 5.5px based on voice volume
let dynamicWidth = minStrokeWidth + (clamped * (maxStrokeWidth - minStrokeWidth))
strokeBorderLayer.borderWidth = dynamicWidth
// 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))
// 2. Dynamic Outer Glow: blooms softly with speech
glowLayer.shadowRadius = 14 + (clamped * 24)
glowLayer.shadowOpacity = Float(0.60 + (clamped * 0.35))
CATransaction.commit()
}
@ -279,58 +239,52 @@ final class SiriOrbView: NSView {
func showRetryState() {
CATransaction.begin()
CATransaction.setDisableActions(false)
CATransaction.setAnimationDuration(0.3)
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
]
haloLayer.shadowColor = NSColor.orange.cgColor
ambientAuraLayer.shadowColor = NSColor.orange.cgColor
CATransaction.setAnimationDuration(0.25)
strokeBorderLayer.borderWidth = 3.5
strokeBorderLayer.borderColor = NSColor.orange.cgColor
glowLayer.shadowColor = NSColor.orange.cgColor
glowLayer.shadowRadius = 22
CATransaction.commit()
}
func showTranscribingState() {
CATransaction.begin()
CATransaction.setDisableActions(true)
self.layer?.sublayerTransform = CATransform3DMakeScale(1.08, 1.08, 1.0)
strokeBorderLayer.borderWidth = 2.5
strokeBorderLayer.borderColor = NSColor.cyan.cgColor
glowLayer.shadowRadius = 18
CATransaction.commit()
}
func showSuccessState() {
CATransaction.begin()
CATransaction.setDisableActions(false)
CATransaction.setAnimationDuration(0.2)
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
]
haloLayer.shadowColor = NSColor.green.cgColor
ambientAuraLayer.shadowColor = NSColor.green.cgColor
CATransaction.setAnimationDuration(0.18)
strokeBorderLayer.borderWidth = 3.0
strokeBorderLayer.borderColor = NSColor(red: 0.1, green: 0.95, blue: 0.5, alpha: 1.0).cgColor
glowLayer.shadowColor = NSColor.green.cgColor
glowLayer.shadowRadius = 20
CATransaction.commit()
}
func showErrorState() {
CATransaction.begin()
CATransaction.setDisableActions(false)
CATransaction.setAnimationDuration(0.2)
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
]
haloLayer.shadowColor = NSColor.red.cgColor
ambientAuraLayer.shadowColor = NSColor.red.cgColor
CATransaction.setAnimationDuration(0.18)
strokeBorderLayer.borderWidth = 3.0
strokeBorderLayer.borderColor = NSColor.red.cgColor
glowLayer.shadowColor = NSColor.red.cgColor
glowLayer.shadowRadius = 20
CATransaction.commit()
}
func reset() {
CATransaction.begin()
CATransaction.setDisableActions(true)
self.layer?.sublayerTransform = CATransform3DIdentity
haloLayer.shadowRadius = 18
ambientAuraLayer.shadowRadius = 38
acousticRippleLayer.transform = CATransform3DIdentity
acousticRippleLayer.opacity = 0.3
strokeBorderLayer.borderWidth = minStrokeWidth
strokeBorderLayer.borderColor = NSColor.white.withAlphaComponent(0.35).cgColor
glowLayer.shadowRadius = 14
glowLayer.shadowOpacity = 0.60
CATransaction.commit()
}
}

102
src/InputBoxManager.swift

@ -4,11 +4,24 @@ import ApplicationServices
public final class InputBoxManager {
public static let shared = InputBoxManager()
private let lock = NSLock()
private var accumulatedStreamedText: String = ""
private let pasteQueue = DispatchQueue(label: "com.soniox.pasteQueue", qos: .userInteractive)
private init() {}
/// Prepares and guarantees that an input box is focused before or at the start of recording.
/// Reset session state at the beginning of speech
public func resetSession() {
lock.lock()
accumulatedStreamedText = ""
lock.unlock()
}
/// Prepares and guarantees that an input box is focused before recording starts.
/// STRICT RULE: If an input box is already focused, NEVER click or steal focus!
public func ensureFocusedInputBox() {
resetSession()
guard AXIsProcessTrusted() else {
print("InputBoxManager: Accessibility not trusted.")
return
@ -21,7 +34,6 @@ public final class InputBoxManager {
var focusedObj: CFTypeRef?
if AXUIElementCopyAttributeValue(appElem, kAXFocusedUIElementAttribute as CFString, &focusedObj) == .success,
focusedObj != nil {
// Already focused, preserve user focus untouched!
return
}
@ -44,6 +56,92 @@ public final class InputBoxManager {
}
}
/// Streams word-by-word delta text directly into the focused input box in real time
public func streamDelta(text: String) {
guard !text.isEmpty else { return }
lock.lock()
accumulatedStreamedText += text
lock.unlock()
pasteQueue.async { [weak self] in
guard let self = self else { return }
let pb = NSPasteboard.general
pb.clearContents()
pb.setString(text, forType: .string)
self.postCleanCmdV()
usleep(25000) // 25ms pacing between chunks
}
}
/// Commits the final speech result: ensures 100% complete text in input box & clipboard
public func commitFinalText(fullText: String, addTrailingSpace: Bool) {
let trimmed = fullText.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return }
let textWithTrailing = addTrailingSpace ? (trimmed + " ") : trimmed
// Always place the full processed text on the system clipboard
let pb = NSPasteboard.general
pb.clearContents()
pb.setString(textWithTrailing, forType: .string)
lock.lock()
let streamed = accumulatedStreamedText
accumulatedStreamedText = ""
lock.unlock()
let cleanStreamed = streamed.trimmingCharacters(in: .whitespacesAndNewlines)
pasteQueue.async { [weak self] in
guard let self = self else { return }
// Case 1: Nothing was streamed live -> Paste entire text
if cleanStreamed.isEmpty {
self.postCleanCmdV()
return
}
// Case 2: Text was streamed live
// Check if there are trailing words recognized only at the very end
if trimmed.count > cleanStreamed.count {
if trimmed.hasPrefix(cleanStreamed) {
let suffix = String(trimmed.dropFirst(cleanStreamed.count))
let suffixToPaste = addTrailingSpace ? (suffix + " ") : suffix
let pb = NSPasteboard.general
pb.clearContents()
pb.setString(suffixToPaste, forType: .string)
self.postCleanCmdV()
} else if addTrailingSpace {
let pb = NSPasteboard.general
pb.clearContents()
pb.setString(" ", forType: .string)
self.postCleanCmdV()
}
} else if addTrailingSpace {
let pb = NSPasteboard.general
pb.clearContents()
pb.setString(" ", forType: .string)
self.postCleanCmdV()
}
}
}
/// Posts Cmd+V using privateState event source to cgAnnotatedSessionEventTap
/// This strips physical Option ⌥ / Shift / Control flags and pastes cleanly across all apps
private func postCleanCmdV() {
let src = CGEventSource(stateID: .privateState)
let vKeyCode: CGKeyCode = 0x09 // 'v'
if let down = CGEvent(keyboardEventSource: src, virtualKey: vKeyCode, keyDown: true),
let up = CGEvent(keyboardEventSource: src, virtualKey: vKeyCode, keyDown: false) {
down.flags = .maskCommand
up.flags = .maskCommand
down.post(tap: .cgAnnotatedSessionEventTap)
up.post(tap: .cgAnnotatedSessionEventTap)
}
}
private func findBestInputElement(in parent: AXUIElement, maxDepth: Int = 8, depth: Int = 0) -> AXUIElement? {
if depth > maxDepth { return nil }

Loading…
Cancel
Save