|
|
@ -17,10 +17,10 @@ public enum DictationMode: String, CaseIterable { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
public enum HotkeyPreset: String, CaseIterable { |
|
|
public enum HotkeyPreset: String, CaseIterable { |
|
|
|
|
|
case micKey = "Microphone Key (🎙️ / F5)" |
|
|
case option = "Option (Hold ⌥)" |
|
|
case option = "Option (Hold ⌥)" |
|
|
case doubleTapFn = "Double-Tap Fn (Globe 🌐)" |
|
|
case doubleTapFn = "Double-Tap Fn (Globe 🌐)" |
|
|
case holdFn = "Hold Fn (Globe 🌐)" |
|
|
case holdFn = "Hold Fn (Globe 🌐)" |
|
|
case f5 = "F5 (Fn + F5)" |
|
|
|
|
|
case controlSpace = "Control + Space" |
|
|
case controlSpace = "Control + Space" |
|
|
case capsLock = "Caps Lock" |
|
|
case capsLock = "Caps Lock" |
|
|
case optionSpace = "Option + Space" |
|
|
case optionSpace = "Option + Space" |
|
|
@ -29,6 +29,8 @@ public enum HotkeyPreset: String, CaseIterable { |
|
|
|
|
|
|
|
|
public var keyCode: UInt32 { |
|
|
public var keyCode: UInt32 { |
|
|
switch self { |
|
|
switch self { |
|
|
|
|
|
case .micKey: |
|
|
|
|
|
return UInt32(kVK_F5) // 96 |
|
|
case .controlSpace, .optionSpace, .cmdShiftSpace: |
|
|
case .controlSpace, .optionSpace, .cmdShiftSpace: |
|
|
return UInt32(kVK_Space) // 49 |
|
|
return UInt32(kVK_Space) // 49 |
|
|
case .option: |
|
|
case .option: |
|
|
@ -39,8 +41,6 @@ public enum HotkeyPreset: String, CaseIterable { |
|
|
return UInt32(kVK_CapsLock) // 57 |
|
|
return UInt32(kVK_CapsLock) // 57 |
|
|
case .f8: |
|
|
case .f8: |
|
|
return UInt32(kVK_F8) |
|
|
return UInt32(kVK_F8) |
|
|
case .f5: |
|
|
|
|
|
return UInt32(kVK_F5) |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -52,7 +52,7 @@ public enum HotkeyPreset: String, CaseIterable { |
|
|
return UInt32(optionKey) |
|
|
return UInt32(optionKey) |
|
|
case .cmdShiftSpace: |
|
|
case .cmdShiftSpace: |
|
|
return UInt32(cmdKey | shiftKey) |
|
|
return UInt32(cmdKey | shiftKey) |
|
|
case .option, .doubleTapFn, .holdFn, .capsLock, .f8, .f5: |
|
|
|
|
|
|
|
|
case .micKey, .option, .doubleTapFn, .holdFn, .capsLock, .f8: |
|
|
return 0 |
|
|
return 0 |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
@ -79,8 +79,8 @@ public final class HotkeyManager { |
|
|
|
|
|
|
|
|
public var currentPreset: HotkeyPreset { |
|
|
public var currentPreset: HotkeyPreset { |
|
|
get { |
|
|
get { |
|
|
let val = UserDefaults.standard.string(forKey: "SonioxHotkeyPreset") ?? HotkeyPreset.option.rawValue |
|
|
|
|
|
return HotkeyPreset(rawValue: val) ?? .option |
|
|
|
|
|
|
|
|
let val = UserDefaults.standard.string(forKey: "SonioxHotkeyPreset") ?? HotkeyPreset.micKey.rawValue |
|
|
|
|
|
return HotkeyPreset(rawValue: val) ?? .micKey |
|
|
} |
|
|
} |
|
|
set { |
|
|
set { |
|
|
UserDefaults.standard.set(newValue.rawValue, forKey: "SonioxHotkeyPreset") |
|
|
UserDefaults.standard.set(newValue.rawValue, forKey: "SonioxHotkeyPreset") |
|
|
@ -108,7 +108,7 @@ public final class HotkeyManager { |
|
|
print("HotkeyManager: Registering hotkey for '\(preset.rawValue)', mode: '\(currentMode.rawValue)'") |
|
|
print("HotkeyManager: Registering hotkey for '\(preset.rawValue)', mode: '\(currentMode.rawValue)'") |
|
|
|
|
|
|
|
|
// 1. Carbon HotKey for multi-key combos (Control+Space, Option+Space, Cmd+Shift+Space, F8) |
|
|
// 1. Carbon HotKey for multi-key combos (Control+Space, Option+Space, Cmd+Shift+Space, F8) |
|
|
if preset != .option && preset != .capsLock && preset != .f5 && preset != .doubleTapFn && preset != .holdFn { |
|
|
|
|
|
|
|
|
if preset != .option && preset != .capsLock && preset != .micKey && preset != .doubleTapFn && preset != .holdFn { |
|
|
var eventTypes = [ |
|
|
var eventTypes = [ |
|
|
EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyPressed)), |
|
|
EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyPressed)), |
|
|
EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyReleased)) |
|
|
EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyReleased)) |
|
|
@ -145,7 +145,7 @@ public final class HotkeyManager { |
|
|
) |
|
|
) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 2. Global Event Tap for high-priority interception (Option, CapsLock, F5, Fn/Globe) |
|
|
|
|
|
|
|
|
// 2. Global Event Tap for high-priority interception (Microphone Key, Option, CapsLock, Fn/Globe) |
|
|
setupEventTap() |
|
|
setupEventTap() |
|
|
|
|
|
|
|
|
// 3. Fallback NSEvent monitor |
|
|
// 3. Fallback NSEvent monitor |
|
|
@ -154,7 +154,7 @@ public final class HotkeyManager { |
|
|
|
|
|
|
|
|
private func triggerAction() { |
|
|
private func triggerAction() { |
|
|
let now = Date().timeIntervalSince1970 |
|
|
let now = Date().timeIntervalSince1970 |
|
|
guard (now - lastTriggerTime) > 0.30 else { return } |
|
|
|
|
|
|
|
|
guard (now - lastTriggerTime) > 0.25 else { return } |
|
|
lastTriggerTime = now |
|
|
lastTriggerTime = now |
|
|
DispatchQueue.main.async { [weak self] in |
|
|
DispatchQueue.main.async { [weak self] in |
|
|
self?.onHotkeyPressed?() |
|
|
self?.onHotkeyPressed?() |
|
|
@ -172,13 +172,23 @@ public final class HotkeyManager { |
|
|
let flags = event.flags.rawValue |
|
|
let flags = event.flags.rawValue |
|
|
let keyCode = event.getIntegerValueField(.keyboardEventKeycode) |
|
|
let keyCode = event.getIntegerValueField(.keyboardEventKeycode) |
|
|
|
|
|
|
|
|
// 1. Control + Space |
|
|
|
|
|
if manager.currentPreset == .controlSpace { |
|
|
|
|
|
if keyCode == 49 && type == .keyDown { // 49 = Space |
|
|
|
|
|
let isCtrl = (flags & CGEventFlags.maskControl.rawValue) != 0 |
|
|
|
|
|
if isCtrl { |
|
|
|
|
|
|
|
|
// 1. Microphone Key (🎙️ / F5) |
|
|
|
|
|
if manager.currentPreset == .micKey { |
|
|
|
|
|
if keyCode == 96 { // 96 = kVK_F5 |
|
|
|
|
|
if type == .keyDown { |
|
|
|
|
|
if !manager.isF5PhysicallyDown { |
|
|
|
|
|
manager.isF5PhysicallyDown = true |
|
|
manager.triggerAction() |
|
|
manager.triggerAction() |
|
|
return nil // Swallow event |
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
return nil // Swallow F5 so it doesn't leak to apps |
|
|
|
|
|
} else if type == .keyUp { |
|
|
|
|
|
manager.isF5PhysicallyDown = false |
|
|
|
|
|
if manager.currentMode == .pushToTalk { |
|
|
|
|
|
DispatchQueue.main.async { |
|
|
|
|
|
manager.onHotkeyReleased?() |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
return nil // Swallow F5 keyUp |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
@ -218,22 +228,12 @@ public final class HotkeyManager { |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 4. F5 Key (Fn + F5) |
|
|
|
|
|
else if manager.currentPreset == .f5 { |
|
|
|
|
|
if keyCode == 96 { // 96 = kVK_F5 |
|
|
|
|
|
if type == .keyDown { |
|
|
|
|
|
if !manager.isF5PhysicallyDown { |
|
|
|
|
|
manager.isF5PhysicallyDown = true |
|
|
|
|
|
|
|
|
// 4. Control + Space |
|
|
|
|
|
else if manager.currentPreset == .controlSpace { |
|
|
|
|
|
if keyCode == 49 && type == .keyDown { // 49 = Space |
|
|
|
|
|
let isCtrl = (flags & CGEventFlags.maskControl.rawValue) != 0 |
|
|
|
|
|
if isCtrl { |
|
|
manager.triggerAction() |
|
|
manager.triggerAction() |
|
|
} |
|
|
|
|
|
return nil |
|
|
|
|
|
} else if type == .keyUp { |
|
|
|
|
|
manager.isF5PhysicallyDown = false |
|
|
|
|
|
if manager.currentMode == .pushToTalk { |
|
|
|
|
|
DispatchQueue.main.async { |
|
|
|
|
|
manager.onHotkeyReleased?() |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
return nil |
|
|
return nil |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|