grammargem/Sources/GrammarGem/System/HotkeyManager.swift
BigMarc 3d749e8139 feat: Implement LiveMonitor for real-time grammar and spelling checks across applications
- Added LiveMonitor class to scan text areas in the frontmost window for grammar and spelling issues.
- Introduced DetectedField struct to represent text areas with issues, including suggestions for corrections.
- Implemented functionality to apply corrections and dismiss suggestions.
- Added Permissions class to manage Accessibility permissions required for monitoring.
- Created SnippetStore for managing text-expansion snippets, including persistence to UserDefaults.
- Developed TextCapture class to read and write text from the frontmost application using Accessibility API and clipboard fallback.
- Implemented TextFieldScanner to traverse the Accessibility tree and collect editable text elements.
- Added TextReplacementCoordinator to orchestrate the text correction process.
- Integrated UpdaterManager for handling application updates using Sparkle.
- Created comprehensive unit tests for various components, including grammar corrections and entitlement checks.
2026-06-18 16:49:55 +02:00

85 lines
3.0 KiB
Swift

import Foundation
import Carbon.HIToolbox
/// Registers system-wide global hotkeys via Carbon `RegisterEventHotKey`.
///
/// This is the no-dependency path. The spec lists `KeyboardShortcuts`
/// (Sindre Sorhus) for a nicer recorder UI; you can swap this implementation
/// for that package without touching callers. User-configurable (defaults ⌘; and ⌘').
final class HotkeyManager {
/// Logical actions a hotkey can fire.
enum Action { case fix, ask }
var onAction: ((Action) -> Void)?
private var refs: [EventHotKeyRef?] = []
private var handler: EventHandlerRef?
private var idToAction: [UInt32: Action] = [:]
init() {
installHandler()
}
deinit {
unregisterAll()
if let handler { RemoveEventHandler(handler) }
}
/// Register the two product hotkeys. Call again to re-register after the
/// user changes a shortcut.
func registerDefaults() {
register(id: 1, keyCode: AppConfig.Hotkey.defaultKeyCode,
modifiers: AppConfig.Hotkey.defaultModifiers, action: .fix)
register(id: 2, keyCode: AppConfig.Hotkey.askKeyCode,
modifiers: AppConfig.Hotkey.askModifiers, action: .ask)
}
@discardableResult
func register(id: UInt32, keyCode: UInt32, modifiers: UInt32, action: Action) -> Bool {
idToAction[id] = action
let hotKeyID = EventHotKeyID(signature: OSType(0x47474D31), id: id) // 'GGM1'
var ref: EventHotKeyRef?
let status = RegisterEventHotKey(
keyCode, modifiers, hotKeyID,
GetEventDispatcherTarget(), 0, &ref)
if status == noErr {
refs.append(ref)
return true
}
Log.system.error("RegisterEventHotKey failed: \(status)")
return false
}
func unregisterAll() {
for ref in refs where ref != nil { UnregisterEventHotKey(ref) }
refs.removeAll()
}
// MARK: - Carbon event plumbing
private func installHandler() {
var spec = EventTypeSpec(
eventClass: OSType(kEventClassKeyboard),
eventKind: UInt32(kEventHotKeyPressed))
let selfPtr = Unmanaged.passUnretained(self).toOpaque()
InstallEventHandler(
GetEventDispatcherTarget(),
{ (_, eventRef, userData) -> OSStatus in
guard let eventRef, let userData else { return noErr }
var hkID = EventHotKeyID()
let err = GetEventParameter(
eventRef, EventParamName(kEventParamDirectObject),
EventParamType(typeEventHotKeyID), nil,
MemoryLayout<EventHotKeyID>.size, nil, &hkID)
guard err == noErr else { return err }
let mgr = Unmanaged<HotkeyManager>.fromOpaque(userData).takeUnretainedValue()
if let action = mgr.idToAction[hkID.id] {
DispatchQueue.main.async { mgr.onAction?(action) }
}
return noErr
},
1, &spec, selfPtr, &handler)
}
}