feat: 菜单栏切换模式,统一使用 Fn 键触发

This commit is contained in:
JOJO 2026-04-24 18:49:13 +08:00
parent 67b3024bda
commit a12258e051
5 changed files with 110 additions and 88 deletions

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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<CGEvent>? in
let monitor = Unmanaged<KeyMonitor>.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<CGEvent>? {
guard type == .flagsChanged else {
return Unmanaged.passUnretained(event)
}
let flags = event.flags.rawValue
_ = event.getIntegerValueField(.keyboardEventKeycode)
let fnActive = (flags & Self.fnMask) != 0
let fnInFlags = (flags & Self.fnMask) != 0
let controlInFlags = (flags & Self.controlMask) != 0
switch type {
case .flagsChanged:
// Fn
if fnInFlags && !isFnPressed {
if fnActive && !isFnPressed {
isFnPressed = true
print("[KeyMonitor] ⚡️ Fn DOWN")
DispatchQueue.main.async { [weak self] in
self?.onFnDown?()
}
return nil
}
if !fnInFlags && isFnPressed {
if !fnActive && isFnPressed {
isFnPressed = false
print("[KeyMonitor] ⚡️ Fn UP")
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:
return Unmanaged.passUnretained(event)
}
}
}

View File

@ -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
// LLMAI
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 {

Binary file not shown.