diff --git a/Sources/App/AppDelegate.swift b/Sources/App/AppDelegate.swift index a8bdb26..e279607 100644 --- a/Sources/App/AppDelegate.swift +++ b/Sources/App/AppDelegate.swift @@ -12,7 +12,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate { private var isRecording = false private var finalTranscription: String = "" - private var currentMode: LLMMode = .input func applicationDidFinishLaunching(_ notification: Notification) { NSApp.setActivationPolicy(.accessory) @@ -74,23 +73,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate { return } - // Fn 键:语音输入模式 + // Fn 键:根据当前模式执行不同功能 keyMonitor.onFnDown = { [weak self] in - self?.currentMode = .input self?.startRecording() } keyMonitor.onFnUp = { [weak self] in self?.stopRecording() } - - // Control 键:AI 助手模式 - keyMonitor.onControlDown = { [weak self] in - self?.currentMode = .assist - self?.startRecording() - } - keyMonitor.onControlUp = { [weak self] in - self?.stopRecording() - } } // MARK: - Recording @@ -103,7 +92,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { recordingPanel.showAnimated() // 根据模式显示不同提示 - let promptText = currentMode == .input ? "Listening..." : "Ask AI..." + let promptText = Config.shared.triggerMode == .input ? "Listening..." : "Ask AI..." recordingPanel.updateText(promptText) let language = Config.shared.language.rawValue @@ -155,14 +144,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate { return } - let config = Config.shared.getLLMConfig(for: currentMode) + // 根据 triggerMode 选择配置和逻辑 + let mode = Config.shared.triggerMode + let config = mode == .input ? Config.shared.inputLLM : Config.shared.assistLLM - switch currentMode { + switch mode { case .input: - // Fn 键模式:语音输入 + 纠错 handleInputMode(text: text, config: config) case .assist: - // Control 键模式:AI 助手对话 handleAssistMode(text: text, config: config) } diff --git a/Sources/App/MenuBarController.swift b/Sources/App/MenuBarController.swift index b362a59..c7ffe88 100644 --- a/Sources/App/MenuBarController.swift +++ b/Sources/App/MenuBarController.swift @@ -6,37 +6,33 @@ final class MenuBarController: NSObject { private let statusItem: NSStatusItem private let menu = NSMenu() private var languageMenuItems: [NSMenuItem] = [] + private var modeMenuItems: [NSMenuItem] = [] - // Input LLM (Fn 键 - 语音输入纠错) + // LLM 配置 private let inputLLMSubmenu = NSMenu() private let inputLLMEnabledItem: NSMenuItem - // Assist LLM (Control 键 - AI 助手) private let assistLLMSubmenu = NSMenu() private let assistLLMEnabledItem: NSMenuItem - // 设置窗口 private let inputSettingsWindow: SettingsWindow private let assistSettingsWindow: SettingsWindow override init() { statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) - // Input LLM 启用项 inputLLMEnabledItem = NSMenuItem( title: "Enable", action: #selector(toggleInputLLM), keyEquivalent: "" ) - // Assist LLM 启用项 assistLLMEnabledItem = NSMenuItem( title: "Enable", action: #selector(toggleAssistLLM), keyEquivalent: "" ) - // 创建两个设置窗口 inputSettingsWindow = SettingsWindow(mode: .input) assistSettingsWindow = SettingsWindow(mode: .assist) @@ -88,7 +84,36 @@ final class MenuBarController: NSObject { menu.addItem(.separator()) - // ===== Input LLM (Fn 键 - 语音输入纠错) ===== + // ===== 模式切换 ===== + let modeMenu = NSMenu() + let inputModeItem = NSMenuItem( + title: "🎤 语音输入", + action: #selector(selectMode(_:)), + keyEquivalent: "" + ) + inputModeItem.representedObject = TriggerMode.input + inputModeItem.state = Config.shared.triggerMode == .input ? .on : .off + modeMenuItems.append(inputModeItem) + modeMenu.addItem(inputModeItem) + + let assistModeItem = NSMenuItem( + title: "💬 AI 助手", + action: #selector(selectMode(_:)), + keyEquivalent: "" + ) + assistModeItem.representedObject = TriggerMode.assist + assistModeItem.state = Config.shared.triggerMode == .assist ? .on : .off + modeMenuItems.append(assistModeItem) + modeMenu.addItem(assistModeItem) + + let modeSubmenuItem = NSMenuItem() + modeSubmenuItem.title = "Mode" + modeSubmenuItem.submenu = modeMenu + menu.addItem(modeSubmenuItem) + + menu.addItem(.separator()) + + // ===== Input LLM ===== inputLLMSubmenu.addItem(inputLLMEnabledItem) inputLLMEnabledItem.target = self inputLLMEnabledItem.action = #selector(toggleInputLLM) @@ -103,13 +128,13 @@ final class MenuBarController: NSObject { inputLLMSubmenu.addItem(inputSettingsItem) let inputSubmenuItem = NSMenuItem() - inputSubmenuItem.title = "🎤 Input LLM (Fn)" + inputSubmenuItem.title = "🎤 Input LLM" inputSubmenuItem.submenu = inputLLMSubmenu menu.addItem(inputSubmenuItem) menu.addItem(.separator()) - // ===== Assist LLM (Control 键 - AI 助手) ===== + // ===== Assist LLM ===== assistLLMSubmenu.addItem(assistLLMEnabledItem) assistLLMEnabledItem.target = self assistLLMEnabledItem.action = #selector(toggleAssistLLM) @@ -124,7 +149,7 @@ final class MenuBarController: NSObject { assistLLMSubmenu.addItem(assistSettingsItem) let assistSubmenuItem = NSMenuItem() - assistSubmenuItem.title = "💬 Assist LLM (Control)" + assistSubmenuItem.title = "💬 Assist LLM" assistSubmenuItem.submenu = assistLLMSubmenu menu.addItem(assistSubmenuItem) @@ -150,12 +175,18 @@ final class MenuBarController: NSObject { assistLLMEnabledItem.state = Config.shared.assistLLM.enabled ? .on : .off } + private func updateModeMenuState() { + for item in modeMenuItems { + guard let mode = item.representedObject as? TriggerMode else { continue } + item.state = mode == Config.shared.triggerMode ? .on : .off + } + } + private func updateLanguageMenuState() { for item in languageMenuItems { guard let lang = item.representedObject as? RecognitionLanguage else { continue } item.state = lang == Config.shared.language ? .on : .off } - // 更新第一项的显示 if let first = menu.items.first { first.title = "Language: \(Config.shared.language.displayName)" } @@ -168,6 +199,13 @@ final class MenuBarController: NSObject { print("[Menu] Language switched to: \(lang.displayName)") } + @objc private func selectMode(_ sender: NSMenuItem) { + guard let mode = sender.representedObject as? TriggerMode else { return } + Config.shared.triggerMode = mode + updateModeMenuState() + print("[Menu] Mode switched to: \(mode)") + } + @objc private func toggleInputLLM() { var config = Config.shared.inputLLM config.enabled.toggle() @@ -186,15 +224,15 @@ final class MenuBarController: NSObject { @objc private func openInputSettings() { inputSettingsWindow.makeKeyAndOrderFront(nil) - NSApp.activate(ignoringOtherApps: true) + NSApplication.shared.activate(ignoringOtherApps: true) } @objc private func openAssistSettings() { assistSettingsWindow.makeKeyAndOrderFront(nil) - NSApp.activate(ignoringOtherApps: true) + NSApplication.shared.activate(ignoringOtherApps: true) } @objc private func quit() { - NSApp.terminate(nil) + NSApplication.shared.terminate(nil) } } diff --git a/Sources/Core/KeyMonitor.swift b/Sources/Core/KeyMonitor.swift index ec29ee9..40e9a94 100644 --- a/Sources/Core/KeyMonitor.swift +++ b/Sources/Core/KeyMonitor.swift @@ -1,23 +1,18 @@ import Cocoa import CoreGraphics -/// 全局热键监听器:Fn 键(语音输入)+ Control 键(AI 助手) +/// 全局热键监听器:只监听 Fn 键 final class KeyMonitor { - // Fn 键:NX_SECONDARYFNMASK + // Fn 键标志 private static let fnMask: UInt64 = 0x00800000 - // Control 键:NX_CONTROLMASK - private static let controlMask: UInt64 = 0x00001000 private var eventTap: CFMachPort? private var runLoopSource: CFRunLoopSource? private var isFnPressed = false - private var isControlPressed = false var onFnDown: (() -> Void)? var onFnUp: (() -> Void)? - var onControlDown: (() -> Void)? - var onControlUp: (() -> Void)? func start() -> Bool { guard eventTap == nil else { return true } @@ -29,8 +24,6 @@ final class KeyMonitor { } let eventMask = (1 << CGEventType.flagsChanged.rawValue) - | (1 << CGEventType.keyDown.rawValue) - | (1 << CGEventType.keyUp.rawValue) let callback: CGEventTapCallBack = { (proxy, type, event, refcon) -> Unmanaged? in let monitor = Unmanaged.fromOpaque(refcon!).takeUnretainedValue() @@ -68,7 +61,7 @@ final class KeyMonitor { CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes) CGEvent.tapEnable(tap: tap, enable: true) - print("[KeyMonitor] ✅ Started (Fn + Control)") + print("[KeyMonitor] ✅ Started") return true } @@ -84,49 +77,30 @@ final class KeyMonitor { } private func handleEvent(proxy: CGEventTapProxy, type: CGEventType, event: CGEvent) -> Unmanaged? { - let flags = event.flags.rawValue - _ = event.getIntegerValueField(.keyboardEventKeycode) - - let fnInFlags = (flags & Self.fnMask) != 0 - let controlInFlags = (flags & Self.controlMask) != 0 - - switch type { - case .flagsChanged: - // Fn 键检测 - if fnInFlags && !isFnPressed { - isFnPressed = true - DispatchQueue.main.async { [weak self] in - self?.onFnDown?() - } - return nil - } - if !fnInFlags && isFnPressed { - isFnPressed = false - DispatchQueue.main.async { [weak self] in - self?.onFnUp?() - } - return nil - } - - // Control 键检测 - if controlInFlags && !isControlPressed { - isControlPressed = true - DispatchQueue.main.async { [weak self] in - self?.onControlDown?() - } - return nil - } - if !controlInFlags && isControlPressed { - isControlPressed = false - DispatchQueue.main.async { [weak self] in - self?.onControlUp?() - } - return nil - } - return Unmanaged.passUnretained(event) - - default: + guard type == .flagsChanged else { return Unmanaged.passUnretained(event) } + + let flags = event.flags.rawValue + let fnActive = (flags & Self.fnMask) != 0 + + if fnActive && !isFnPressed { + isFnPressed = true + print("[KeyMonitor] ⚡️ Fn DOWN") + DispatchQueue.main.async { [weak self] in + self?.onFnDown?() + } + return nil + } + if !fnActive && isFnPressed { + isFnPressed = false + print("[KeyMonitor] ⚡️ Fn UP") + DispatchQueue.main.async { [weak self] in + self?.onFnUp?() + } + return nil + } + + return Unmanaged.passUnretained(event) } } diff --git a/Sources/Utils/Config.swift b/Sources/Utils/Config.swift index a053f88..7525157 100644 --- a/Sources/Utils/Config.swift +++ b/Sources/Utils/Config.swift @@ -19,8 +19,13 @@ enum RecognitionLanguage: String, CaseIterable { } enum LLMMode { - case input // Fn 键:语音输入纠错 - case assist // Control 键:AI 助手对话 + case input // 语音输入纠错 + case assist // AI 助手对话 +} + +enum TriggerMode: String { + case input = "input" // 语音输入 + case assist = "assist" // AI 助手 } struct LLMConfig { @@ -43,14 +48,15 @@ final class Config { private enum Keys { static let language = "recognition_language" + static let triggerMode = "trigger_mode" - // Fn 键模式(语音输入纠错) + // 输入模式 LLM(语音输入纠错) static let inputLLMEnabled = "input_llm_enabled" static let inputLLMBaseURL = "input_llm_base_url" static let inputLLMAPIKey = "input_llm_api_key" static let inputLLMModel = "input_llm_model" - // Control 键模式(AI 助手) + // 助手模式 LLM(AI 助手对话) static let assistLLMEnabled = "assist_llm_enabled" static let assistLLMBaseURL = "assist_llm_base_url" static let assistLLMAPIKey = "assist_llm_api_key" @@ -70,7 +76,22 @@ final class Config { } } - // MARK: - Fn 键模式配置(语音输入纠错) + // MARK: - 触发模式(菜单栏切换) + + var triggerMode: TriggerMode { + get { + guard let raw = defaults.string(forKey: Keys.triggerMode), + let mode = TriggerMode(rawValue: raw) else { + return .input + } + return mode + } + set { + defaults.set(newValue.rawValue, forKey: Keys.triggerMode) + } + } + + // MARK: - 输入模式配置(语音输入纠错) var inputLLM: LLMConfig { get { diff --git a/VoiceInput-20260424.dmg b/VoiceInput-20260424.dmg index aaed257..7cf9fa4 100644 Binary files a/VoiceInput-20260424.dmg and b/VoiceInput-20260424.dmg differ