vOOice/VoiceInk/HotkeyManager.swift
2025-03-12 09:06:14 +05:45

311 lines
12 KiB
Swift

import Foundation
import KeyboardShortcuts
import Carbon
import AppKit
extension KeyboardShortcuts.Name {
static let toggleMiniRecorder = Self("toggleMiniRecorder")
static let escapeRecorder = Self("escapeRecorder")
static let toggleEnhancement = Self("toggleEnhancement")
// Prompt selection shortcuts
static let selectPrompt1 = Self("selectPrompt1")
static let selectPrompt2 = Self("selectPrompt2")
static let selectPrompt3 = Self("selectPrompt3")
static let selectPrompt4 = Self("selectPrompt4")
static let selectPrompt5 = Self("selectPrompt5")
static let selectPrompt6 = Self("selectPrompt6")
static let selectPrompt7 = Self("selectPrompt7")
static let selectPrompt8 = Self("selectPrompt8")
static let selectPrompt9 = Self("selectPrompt9")
}
@MainActor
class HotkeyManager: ObservableObject {
@Published var isListening = false
@Published var isShortcutConfigured = false
@Published var isPushToTalkEnabled: Bool {
didSet {
UserDefaults.standard.set(isPushToTalkEnabled, forKey: "isPushToTalkEnabled")
if !isPushToTalkEnabled {
isRightOptionKeyPressed = false
isFnKeyPressed = false
isRightCommandKeyPressed = false
isRightShiftKeyPressed = false
}
setupKeyMonitors()
}
}
@Published var pushToTalkKey: PushToTalkKey {
didSet {
UserDefaults.standard.set(pushToTalkKey.rawValue, forKey: "pushToTalkKey")
isRightOptionKeyPressed = false
isFnKeyPressed = false
isRightCommandKeyPressed = false
isRightShiftKeyPressed = false
}
}
private var whisperState: WhisperState
private var isRightOptionKeyPressed = false
private var isFnKeyPressed = false
private var isRightCommandKeyPressed = false
private var isRightShiftKeyPressed = false
private var localKeyMonitor: Any?
private var globalKeyMonitor: Any?
private var visibilityTask: Task<Void, Never>?
// Add cooldown management
private var lastShortcutTriggerTime: Date?
private let shortcutCooldownInterval: TimeInterval = 0.5 // 500ms cooldown
enum PushToTalkKey: String, CaseIterable {
case rightOption = "rightOption"
case fn = "fn"
case rightCommand = "rightCommand"
case rightShift = "rightShift"
var displayName: String {
switch self {
case .rightOption: return "Right Option (⌥)"
case .fn: return "Fn"
case .rightCommand: return "Right Command (⌘)"
case .rightShift: return "Right Shift (⇧)"
}
}
}
init(whisperState: WhisperState) {
self.isPushToTalkEnabled = UserDefaults.standard.bool(forKey: "isPushToTalkEnabled")
self.pushToTalkKey = PushToTalkKey(rawValue: UserDefaults.standard.string(forKey: "pushToTalkKey") ?? "") ?? .rightCommand
self.whisperState = whisperState
updateShortcutStatus()
setupEnhancementShortcut()
// Start observing mini recorder visibility
setupVisibilityObserver()
}
private func setupVisibilityObserver() {
visibilityTask = Task { @MainActor in
for await isVisible in whisperState.$isMiniRecorderVisible.values {
if isVisible {
setupEscapeShortcut()
// Set Command+E shortcut when visible
KeyboardShortcuts.setShortcut(.init(.e, modifiers: .command), for: .toggleEnhancement)
setupPromptShortcuts()
} else {
removeEscapeShortcut()
// Remove Command+E shortcut when not visible
KeyboardShortcuts.setShortcut(nil, for: .toggleEnhancement)
removePromptShortcuts()
}
}
}
}
private func setupEscapeShortcut() {
// Set ESC as the shortcut using KeyboardShortcuts native approach
KeyboardShortcuts.setShortcut(.init(.escape), for: .escapeRecorder)
// Setup handler
KeyboardShortcuts.onKeyDown(for: .escapeRecorder) { [weak self] in
Task { @MainActor in
guard let self = self,
await self.whisperState.isMiniRecorderVisible else { return }
SoundManager.shared.playEscSound()
await self.whisperState.dismissMiniRecorder()
}
}
}
private func removeEscapeShortcut() {
KeyboardShortcuts.setShortcut(nil, for: .escapeRecorder)
}
private func setupEnhancementShortcut() {
// Only setup the handler, don't set the shortcut here
// The shortcut will be set/removed based on visibility
KeyboardShortcuts.onKeyDown(for: .toggleEnhancement) { [weak self] in
Task { @MainActor in
guard let self = self,
await self.whisperState.isMiniRecorderVisible,
let enhancementService = await self.whisperState.getEnhancementService() else { return }
enhancementService.isEnhancementEnabled.toggle()
}
}
}
private func removeEnhancementShortcut() {
KeyboardShortcuts.setShortcut(nil, for: .toggleEnhancement)
}
private func setupPromptShortcuts() {
// Set up Command+1 through Command+9 shortcuts with proper key definitions
KeyboardShortcuts.setShortcut(.init(.one, modifiers: .command), for: .selectPrompt1)
KeyboardShortcuts.setShortcut(.init(.two, modifiers: .command), for: .selectPrompt2)
KeyboardShortcuts.setShortcut(.init(.three, modifiers: .command), for: .selectPrompt3)
KeyboardShortcuts.setShortcut(.init(.four, modifiers: .command), for: .selectPrompt4)
KeyboardShortcuts.setShortcut(.init(.five, modifiers: .command), for: .selectPrompt5)
KeyboardShortcuts.setShortcut(.init(.six, modifiers: .command), for: .selectPrompt6)
KeyboardShortcuts.setShortcut(.init(.seven, modifiers: .command), for: .selectPrompt7)
KeyboardShortcuts.setShortcut(.init(.eight, modifiers: .command), for: .selectPrompt8)
KeyboardShortcuts.setShortcut(.init(.nine, modifiers: .command), for: .selectPrompt9)
// Setup handlers for each shortcut
setupPromptHandler(for: .selectPrompt1, index: 0)
setupPromptHandler(for: .selectPrompt2, index: 1)
setupPromptHandler(for: .selectPrompt3, index: 2)
setupPromptHandler(for: .selectPrompt4, index: 3)
setupPromptHandler(for: .selectPrompt5, index: 4)
setupPromptHandler(for: .selectPrompt6, index: 5)
setupPromptHandler(for: .selectPrompt7, index: 6)
setupPromptHandler(for: .selectPrompt8, index: 7)
setupPromptHandler(for: .selectPrompt9, index: 8)
}
private func setupPromptHandler(for shortcutName: KeyboardShortcuts.Name, index: Int) {
KeyboardShortcuts.onKeyDown(for: shortcutName) { [weak self] in
Task { @MainActor in
guard let self = self,
await self.whisperState.isMiniRecorderVisible,
let enhancementService = await self.whisperState.getEnhancementService() else { return }
let prompts = enhancementService.allPrompts
if index < prompts.count {
// Enable AI enhancement if it's not already enabled
if !enhancementService.isEnhancementEnabled {
enhancementService.isEnhancementEnabled = true
}
// Switch to the selected prompt
enhancementService.setActivePrompt(prompts[index])
}
}
}
}
private func removePromptShortcuts() {
// Remove Command+1 through Command+9 shortcuts
KeyboardShortcuts.setShortcut(nil, for: .selectPrompt1)
KeyboardShortcuts.setShortcut(nil, for: .selectPrompt2)
KeyboardShortcuts.setShortcut(nil, for: .selectPrompt3)
KeyboardShortcuts.setShortcut(nil, for: .selectPrompt4)
KeyboardShortcuts.setShortcut(nil, for: .selectPrompt5)
KeyboardShortcuts.setShortcut(nil, for: .selectPrompt6)
KeyboardShortcuts.setShortcut(nil, for: .selectPrompt7)
KeyboardShortcuts.setShortcut(nil, for: .selectPrompt8)
KeyboardShortcuts.setShortcut(nil, for: .selectPrompt9)
}
func updateShortcutStatus() {
isShortcutConfigured = KeyboardShortcuts.getShortcut(for: .toggleMiniRecorder) != nil
if isShortcutConfigured {
setupShortcutHandler()
setupKeyMonitors()
} else {
removeKeyMonitors()
}
}
private func setupShortcutHandler() {
KeyboardShortcuts.onKeyUp(for: .toggleMiniRecorder) { [weak self] in
Task { @MainActor in
await self?.handleShortcutTriggered()
}
}
}
private func handleShortcutTriggered() async {
// Check cooldown
if let lastTrigger = lastShortcutTriggerTime,
Date().timeIntervalSince(lastTrigger) < shortcutCooldownInterval {
return // Still in cooldown period
}
// Update last trigger time
lastShortcutTriggerTime = Date()
// Handle the shortcut
await whisperState.handleToggleMiniRecorder()
}
private func removeKeyMonitors() {
if let monitor = localKeyMonitor {
NSEvent.removeMonitor(monitor)
localKeyMonitor = nil
}
if let monitor = globalKeyMonitor {
NSEvent.removeMonitor(monitor)
globalKeyMonitor = nil
}
}
private func setupKeyMonitors() {
guard isPushToTalkEnabled else {
removeKeyMonitors()
return
}
// Remove existing monitors first
removeKeyMonitors()
// Local monitor for when app is in foreground
localKeyMonitor = NSEvent.addLocalMonitorForEvents(matching: .flagsChanged) { [weak self] event in
Task { @MainActor in
await self?.handlePushToTalkKey(event)
}
return event
}
// Global monitor for when app is in background
globalKeyMonitor = NSEvent.addGlobalMonitorForEvents(matching: .flagsChanged) { [weak self] event in
Task { @MainActor in
await self?.handlePushToTalkKey(event)
}
}
}
private func handlePushToTalkKey(_ event: NSEvent) async {
// Only handle push-to-talk if enabled and configured
guard isPushToTalkEnabled && isShortcutConfigured else { return }
let keyState: Bool
switch pushToTalkKey {
case .rightOption:
keyState = event.modifierFlags.contains(.option) && event.keyCode == 0x3D
guard keyState != isRightOptionKeyPressed else { return }
isRightOptionKeyPressed = keyState
case .fn:
keyState = event.modifierFlags.contains(.function)
guard keyState != isFnKeyPressed else { return }
isFnKeyPressed = keyState
case .rightCommand:
keyState = event.modifierFlags.contains(.command) && event.keyCode == 0x36
guard keyState != isRightCommandKeyPressed else { return }
isRightCommandKeyPressed = keyState
case .rightShift:
keyState = event.modifierFlags.contains(.shift) && event.keyCode == 0x3C
guard keyState != isRightShiftKeyPressed else { return }
isRightShiftKeyPressed = keyState
}
// Toggle recording based on key state
if whisperState.isMiniRecorderVisible != keyState {
await whisperState.handleToggleMiniRecorder()
}
}
deinit {
visibilityTask?.cancel()
Task { @MainActor in
removeKeyMonitors()
removeEscapeShortcut()
removeEnhancementShortcut()
}
}
}