diff --git a/Info.plist b/Info.plist
index d378e2c..ea104d3 100644
--- a/Info.plist
+++ b/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 2.08
+ 2.09
CFBundleVersion
1
LSMinimumSystemVersion
diff --git a/src/AppDelegate.swift b/src/AppDelegate.swift
index 96b5cbb..4779c24 100644
--- a/src/AppDelegate.swift
+++ b/src/AppDelegate.swift
@@ -106,16 +106,13 @@ public final class AppDelegate: NSObject, NSApplicationDelegate {
}
}
- // Escape Key Handler: Immediately aborts active recording, transcription, recovery, and hides the green orb!
+ // Escape Key Handler: Non-exclusive! Both Soniox Voice AND the active app (Telegram, etc.) execute Escape!
HotkeyManager.shared.onEscapePressed = { [weak self] in
- guard let self = self else { return false }
+ guard let self = self else { return }
let isActive = self.audioRecorder.isRecording || self.isBusyFinalizing || (self.activeSession != nil) || HUDOverlayController.shared.isVisible
- guard isActive else { return false }
+ guard isActive else { return }
- DispatchQueue.main.async {
- self.cancelEverythingImmediately()
- }
- return true // Consume Escape so it only cancels Soniox Voice
+ self.cancelEverythingImmediately()
}
HotkeyManager.shared.registerHotkeys()
diff --git a/src/HUDOverlay.swift b/src/HUDOverlay.swift
index 08e006f..5e19945 100644
--- a/src/HUDOverlay.swift
+++ b/src/HUDOverlay.swift
@@ -88,6 +88,7 @@ public final class HUDOverlayController {
window.orderFrontRegardless()
window.alphaValue = 1.0
self.isVisible = true
+ HotkeyManager.shared.setEscapeMonitoringActive(true)
}
orb.updateAudioLevel(level, isLagging: isLagging, pingQuality: pingQuality)
@@ -96,6 +97,7 @@ public final class HUDOverlayController {
window.orderFrontRegardless()
window.alphaValue = 1.0
self.isVisible = true
+ HotkeyManager.shared.setEscapeMonitoringActive(true)
}
orb.showRetryState(reason: reason)
@@ -104,6 +106,7 @@ public final class HUDOverlayController {
window.orderFrontRegardless()
window.alphaValue = 1.0
self.isVisible = true
+ HotkeyManager.shared.setEscapeMonitoringActive(true)
}
orb.showTranscribingState()
@@ -120,6 +123,7 @@ public final class HUDOverlayController {
window.orderFrontRegardless()
window.alphaValue = 1.0
self.isVisible = true
+ HotkeyManager.shared.setEscapeMonitoringActive(true)
}
orb.showErrorState(message: message)
let work = DispatchWorkItem { [weak self] in
@@ -138,6 +142,7 @@ public final class HUDOverlayController {
self.autoHideWorkItem?.cancel()
self.autoHideWorkItem = nil
self.isVisible = false
+ HotkeyManager.shared.setEscapeMonitoringActive(false)
if let window = self.window {
window.alphaValue = 0.0
window.orderOut(nil)
@@ -152,6 +157,7 @@ public final class HUDOverlayController {
self.autoHideWorkItem?.cancel()
self.autoHideWorkItem = nil
self.isVisible = false
+ HotkeyManager.shared.setEscapeMonitoringActive(false)
NSAnimationContext.runAnimationGroup({ context in
context.duration = 0.15
window.animator().alphaValue = 0.0
diff --git a/src/HotkeyManager.swift b/src/HotkeyManager.swift
index 6fbbb6a..341cbc0 100644
--- a/src/HotkeyManager.swift
+++ b/src/HotkeyManager.swift
@@ -63,7 +63,7 @@ public final class HotkeyManager {
public var onHotkeyPressed: (() -> Void)?
public var onHotkeyReleased: (() -> Void)?
- public var onEscapePressed: (() -> Bool)? // Returns true if Soniox was active and consumed Escape
+ public var onEscapePressed: (() -> Void)? // Non-exclusive: both Soniox and active app (Telegram, etc.) receive Escape!
private let stateLock = NSLock()
private var hotKeyRef: EventHotKeyRef?
@@ -72,6 +72,8 @@ public final class HotkeyManager {
private var runLoopSource: CFRunLoopSource?
private var globalMonitor: Any?
private var localMonitor: Any?
+ private var escapePollTimer: Timer?
+ private var wasEscapePhysicallyDown = false
private var isOptionPhysicallyDown = false
private var isCapsLockPhysicallyDown = false
@@ -85,6 +87,7 @@ public final class HotkeyManager {
}
private var lastFnPressTime: TimeInterval = 0
private var lastTriggerTime: TimeInterval = 0
+ private var lastEscapeTime: TimeInterval = 0
public var currentPreset: HotkeyPreset {
get {
@@ -146,9 +149,50 @@ public final class HotkeyManager {
isF5PhysicallyDown = false
isCapsLockPhysicallyDown = false
isFnPhysicallyDown = false
+ wasEscapePhysicallyDown = false
stateLock.unlock()
}
+ /// Enables/disables ultra-lightweight 60Hz hardware Escape key state polling while Soniox is active (recording/transcribing/retrying/HUD visible).
+ /// Because it reads hardware HID state directly (CGEventSource.keyState), it NEVER conflicts with or blocks Telegram or other apps' Escape hotkeys,
+ /// and works 100% reliably regardless of TCC Input Monitoring ad-hoc signature state!
+ public func setEscapeMonitoringActive(_ active: Bool) {
+ DispatchQueue.main.async { [weak self] in
+ guard let self = self else { return }
+ self.escapePollTimer?.invalidate()
+ self.escapePollTimer = nil
+ self.wasEscapePhysicallyDown = false
+
+ if active {
+ self.escapePollTimer = Timer.scheduledTimer(withTimeInterval: 0.016, repeats: true) { [weak self] _ in
+ guard let self = self else { return }
+ let isDown = CGEventSource.keyState(.combinedSessionState, key: 53) || CGEventSource.keyState(.hidSystemState, key: 53)
+ if isDown && !self.wasEscapePhysicallyDown {
+ self.wasEscapePhysicallyDown = true
+ self.triggerEscape()
+ } else if !isDown {
+ self.wasEscapePhysicallyDown = false
+ }
+ }
+ }
+ }
+ }
+
+ private func triggerEscape() {
+ stateLock.lock()
+ let now = Date().timeIntervalSince1970
+ guard (now - lastEscapeTime) > 0.15 else {
+ stateLock.unlock()
+ return
+ }
+ lastEscapeTime = now
+ stateLock.unlock()
+
+ DispatchQueue.main.async { [weak self] in
+ self?.onEscapePressed?()
+ }
+ }
+
public func registerHotkeys() {
unregisterHotkeys()
resetKeyState()
@@ -194,10 +238,10 @@ public final class HotkeyManager {
)
}
- // 2. Global Event Tap for high-priority interception (Microphone Key, Option, CapsLock, Fn/Globe, Escape)
+ // 2. Global Event Tap for high-priority interception
setupEventTap()
- // 3. Fallback NSEvent monitor (active ONLY if CGEventTap is disabled/unavailable)
+ // 3. Always-active non-exclusive NSEvent monitor for modifier & Escape pass-through
setupGlobalMonitor()
}
@@ -236,11 +280,11 @@ public final class HotkeyManager {
let flags = event.flags.rawValue
let keyCode = event.getIntegerValueField(.keyboardEventKeycode)
- // 0. Escape Key (kVK_Escape = 53): Instant Cancel of active recording / transcription / recovery / HUD
+ // 0. Escape Key (kVK_Escape = 53): Non-blocking pass-through!
+ // Both Soniox Voice AND the active app (Telegram, Chrome, etc.) execute their Escape action simultaneously!
if keyCode == 53 && type == .keyDown {
- if let consumed = manager.onEscapePressed?(), consumed {
- return nil // Swallow Escape so it only cancels Soniox and doesn't close user's dialog/page
- }
+ manager.triggerEscape()
+ return Unmanaged.passRetained(event) // NEVER swallow Escape so Telegram/apps also receive it!
}
// 1. Microphone Key (🎙️ / F5)
@@ -388,25 +432,34 @@ public final class HotkeyManager {
NSEvent.removeMonitor(mon)
self.globalMonitor = nil
}
+ if let lmon = localMonitor {
+ NSEvent.removeMonitor(lmon)
+ self.localMonitor = nil
+ }
- // Active backup monitor: ONLY acts if CGEventTap is nil or disabled by OS, eliminating dual-trigger race conditions!
+ // Local monitor if Soniox menu/HUD is ever key
+ localMonitor = NSEvent.addLocalMonitorForEvents(matching: [.keyDown, .flagsChanged]) { [weak self] event in
+ if event.keyCode == 53 && event.type == .keyDown {
+ self?.triggerEscape()
+ }
+ return event // Pass through
+ }
+
+ // Global passive monitor: always active alongside EventTap (deduplicated via stateLock)
globalMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.flagsChanged, .keyDown, .keyUp]) { [weak self] event in
guard let self = self else { return }
- if let tap = self.eventTapPort, CGEvent.tapIsEnabled(tap: tap) {
- return // Primary CGEventTap is healthy and handling events; do not double-process
- }
-
- // Wake up eventTap if it was disabled
- if let tap = self.eventTapPort {
+ // Wake up eventTap if it was disabled by OS
+ if let tap = self.eventTapPort, !CGEvent.tapIsEnabled(tap: tap) {
CGEvent.tapEnable(tap: tap, enable: true)
}
let flags = event.modifierFlags
let keyCode = event.keyCode
+ // Non-exclusive Escape pass-through
if keyCode == 53 && event.type == .keyDown {
- _ = self.onEscapePressed?()
+ self.triggerEscape()
return
}
@@ -456,6 +509,8 @@ public final class HotkeyManager {
public func unregisterHotkeys() {
resetKeyState()
+ escapePollTimer?.invalidate()
+ escapePollTimer = nil
if let ref = hotKeyRef {
UnregisterEventHotKey(ref)
hotKeyRef = nil
@@ -474,5 +529,9 @@ public final class HotkeyManager {
NSEvent.removeMonitor(mon)
self.globalMonitor = nil
}
+ if let lmon = localMonitor {
+ NSEvent.removeMonitor(lmon)
+ self.localMonitor = nil
+ }
}
}