c2f07bd8d2
Introduce a standalone macOS menu-bar app (OSGKeyboardMac) that reuses the platform-agnostic OSGKeyboardShared core: record -> cloud/local ASR -> polish -> insert. Local mode uses Qwen3-ASR via mlx-swift-asr (macOS 15+, Apple Silicon); iOS targets stay zero-SPM. Harden iCloud sync for multi-device correctness: - Per-field settings merge (appSettings.v2) so concurrent edits no longer clobber each other's unrelated fields. - Per-device usage statistics (G-Counter) that sum instead of max(). - Tombstoned dictionary/history merge so deletes propagate and entries can't resurrect. - API keys replicate via iCloud Keychain, never iCloud KVS JSON; pulling a legacy blob without key fields no longer wipes local Keychain entries. - Add a low-risk "Sync Now" action in Settings. Fix Flow keyboard mic state: stay orange until the host publishes a real ready contract, share a single MicVoiceAvailability gate, and self-heal stale cross-process heartbeat jitter instead of getting stuck. Extract shared storage (SpeechHistoryStore/UsageStatisticsStore, ConfigurationStore) into OSGKeyboardShared and add tests for the new sync/merge logic.
68 lines
1.9 KiB
Swift
68 lines
1.9 KiB
Swift
// MacHotkeyService.swift
|
|
// OSGKeyboard · Mac
|
|
//
|
|
// Global hold-to-talk: while Option (⌥) is held, dictation runs. Mirrors
|
|
// Typeless / SayIt push-to-talk from any foreground app.
|
|
|
|
import AppKit
|
|
import Foundation
|
|
|
|
@MainActor
|
|
final class MacHotkeyService {
|
|
var onPressBegan: (() -> Void)?
|
|
var onPressEnded: (() -> Void)?
|
|
|
|
private var globalFlagsMonitor: Any?
|
|
private var localFlagsMonitor: Any?
|
|
private var optionHeld = false
|
|
private var isEnabled = true
|
|
|
|
func setEnabled(_ enabled: Bool) {
|
|
isEnabled = enabled
|
|
if !enabled, optionHeld {
|
|
optionHeld = false
|
|
onPressEnded?()
|
|
}
|
|
}
|
|
|
|
func start() {
|
|
guard globalFlagsMonitor == nil else { return }
|
|
_ = MacTextInsertionService.requestAccessibilityIfNeeded()
|
|
|
|
globalFlagsMonitor = NSEvent.addGlobalMonitorForEvents(matching: .flagsChanged) { [weak self] event in
|
|
Task { @MainActor in self?.handleFlagsChanged(event) }
|
|
}
|
|
localFlagsMonitor = NSEvent.addLocalMonitorForEvents(matching: .flagsChanged) { [weak self] event in
|
|
Task { @MainActor in self?.handleFlagsChanged(event) }
|
|
return event
|
|
}
|
|
}
|
|
|
|
func stop() {
|
|
if let globalFlagsMonitor {
|
|
NSEvent.removeMonitor(globalFlagsMonitor)
|
|
self.globalFlagsMonitor = nil
|
|
}
|
|
if let localFlagsMonitor {
|
|
NSEvent.removeMonitor(localFlagsMonitor)
|
|
self.localFlagsMonitor = nil
|
|
}
|
|
if optionHeld {
|
|
optionHeld = false
|
|
onPressEnded?()
|
|
}
|
|
}
|
|
|
|
private func handleFlagsChanged(_ event: NSEvent) {
|
|
guard isEnabled else { return }
|
|
let optionDown = event.modifierFlags.contains(.option)
|
|
if optionDown, !optionHeld {
|
|
optionHeld = true
|
|
onPressBegan?()
|
|
} else if !optionDown, optionHeld {
|
|
optionHeld = false
|
|
onPressEnded?()
|
|
}
|
|
}
|
|
}
|