feat: 快速双击 Fn 键切换语音输入/AI 助手模式

This commit is contained in:
JOJO 2026-04-24 23:34:23 +08:00
parent 847ae72e49
commit ad72907e89
3 changed files with 67 additions and 7 deletions

View File

@ -80,6 +80,18 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
keyMonitor.onFnUp = { [weak self] in
self?.stopRecording()
}
keyMonitor.onFnDoubleClick = { [weak self] in
self?.toggleModeViaDoubleClick()
}
}
// MARK: - Double-Click Mode Switch
private func toggleModeViaDoubleClick() {
let newMode: TriggerMode = Config.shared.triggerMode == .input ? .assist : .input
Config.shared.triggerMode = newMode
menuBarController.refreshModeMenu()
print("[App] Double-click: mode switched to \(newMode)")
}
// MARK: - Recording

View File

@ -201,6 +201,10 @@ final class MenuBarController: NSObject {
return true
}
func refreshModeMenu() {
updateModeMenuState()
}
private func updateInputLLMMenuState() {
inputLLMEnabledItem.state = Config.shared.inputLLM.enabled ? .on : .off
}

View File

@ -1,18 +1,28 @@
import Cocoa
import CoreGraphics
/// Fn
/// Fn
final class KeyMonitor {
// Fn
private static let fnMask: UInt64 = 0x00800000
//
private static let doubleClickWindow: TimeInterval = 0.40 //
private static let longPressThreshold: TimeInterval = 0.20 //
private var eventTap: CFMachPort?
private var runLoopSource: CFRunLoopSource?
private var isFnPressed = false
//
private var lastFnUpTime: TimeInterval = 0
private var fnDownTime: TimeInterval = 0
private var longPressTimer: Timer?
var onFnDown: (() -> Void)?
var onFnUp: (() -> Void)?
var onFnDoubleClick: (() -> Void)? // Fn
func start() -> Bool {
guard eventTap == nil else { return true }
@ -86,17 +96,51 @@ final class KeyMonitor {
if fnActive && !isFnPressed {
isFnPressed = true
print("[KeyMonitor] ⚡️ Fn DOWN")
DispatchQueue.main.async { [weak self] in
self?.onFnDown?()
fnDownTime = ProcessInfo.processInfo.systemUptime
// < 400ms
let elapsed = fnDownTime - lastFnUpTime
if elapsed > 0 && elapsed < Self.doubleClickWindow {
// timer
longPressTimer?.invalidate()
longPressTimer = nil
print("[KeyMonitor] ⚡️ Fn DOUBLE-CLICK → switch mode")
DispatchQueue.main.async { [weak self] in
self?.onFnDoubleClick?()
}
return nil
}
// 200ms
print("[KeyMonitor] ⚡️ Fn DOWN (waiting for long-press/double-click)")
longPressTimer?.invalidate()
longPressTimer = Timer.scheduledTimer(withTimeInterval: Self.longPressThreshold, repeats: false) { [weak self] _ in
guard let self = self, self.isFnPressed else { return }
print("[KeyMonitor] ⚡️ Fn LONG-PRESS → start recording")
self.longPressTimer = nil
DispatchQueue.main.async {
self.onFnDown?()
}
}
return nil
}
if !fnActive && isFnPressed {
isFnPressed = false
print("[KeyMonitor] ⚡️ Fn UP")
DispatchQueue.main.async { [weak self] in
self?.onFnUp?()
lastFnUpTime = ProcessInfo.processInfo.systemUptime
// timer/
longPressTimer?.invalidate()
longPressTimer = nil
print("[KeyMonitor] ⚡️ Fn UP (was pressed for \(String(format: "%.0f", (lastFnUpTime - fnDownTime) * 1000))ms)")
// timer onFnUp
//
if lastFnUpTime - fnDownTime >= Self.longPressThreshold {
// onFnUp
DispatchQueue.main.async { [weak self] in
self?.onFnUp?()
}
}
return nil
}