93 lines
3.9 KiB
Swift
93 lines
3.9 KiB
Swift
import AppKit
|
|
import Carbon.HIToolbox
|
|
|
|
/// Serializable representation of a keyboard shortcut (modifier flags + key code).
|
|
/// Stored in UserDefaults as a JSON string.
|
|
struct HotkeyBinding: Codable, Equatable {
|
|
var modifiers: UInt // NSEvent.ModifierFlags.rawValue, masked to cmd/shift/ctrl/opt
|
|
var keyCode: UInt16
|
|
|
|
/// Checks whether the given NSEvent matches this binding.
|
|
func matches(_ event: NSEvent) -> Bool {
|
|
let mask = event.modifierFlags.intersection(.deviceIndependentFlagsMask)
|
|
let relevantFlags: NSEvent.ModifierFlags = [.command, .shift, .control, .option]
|
|
return mask.intersection(relevantFlags).rawValue == modifiers
|
|
&& event.keyCode == keyCode
|
|
}
|
|
|
|
/// Human-readable label like "⌘⏎" or "⌘⇧T".
|
|
var displayString: String {
|
|
var parts: [String] = []
|
|
let flags = NSEvent.ModifierFlags(rawValue: modifiers)
|
|
if flags.contains(.control) { parts.append("⌃") }
|
|
if flags.contains(.option) { parts.append("⌥") }
|
|
if flags.contains(.shift) { parts.append("⇧") }
|
|
if flags.contains(.command) { parts.append("⌘") }
|
|
parts.append(keyName)
|
|
return parts.joined()
|
|
}
|
|
|
|
private var keyName: String {
|
|
switch keyCode {
|
|
case 36: return "⏎"
|
|
case 48: return "⇥"
|
|
case 49: return "Space"
|
|
case 51: return "⌫"
|
|
case 53: return "⎋"
|
|
case 123: return "←"
|
|
case 124: return "→"
|
|
case 125: return "↓"
|
|
case 126: return "↑"
|
|
default:
|
|
// Try to get the character from the key code
|
|
let src = TISCopyCurrentKeyboardInputSource().takeRetainedValue()
|
|
let layoutDataRef = TISGetInputSourceProperty(src, kTISPropertyUnicodeKeyLayoutData)
|
|
if let layoutDataRef = layoutDataRef {
|
|
let layoutData = unsafeBitCast(layoutDataRef, to: CFData.self) as Data
|
|
var deadKeyState: UInt32 = 0
|
|
var length = 0
|
|
var chars = [UniChar](repeating: 0, count: 4)
|
|
layoutData.withUnsafeBytes { ptr in
|
|
let layoutPtr = ptr.baseAddress!.assumingMemoryBound(to: UCKeyboardLayout.self)
|
|
UCKeyTranslate(
|
|
layoutPtr,
|
|
keyCode,
|
|
UInt16(kUCKeyActionDisplay),
|
|
0, // no modifiers for the base character
|
|
UInt32(LMGetKbdType()),
|
|
UInt32(kUCKeyTranslateNoDeadKeysBit),
|
|
&deadKeyState,
|
|
4,
|
|
&length,
|
|
&chars
|
|
)
|
|
}
|
|
if length > 0 {
|
|
return String(utf16CodeUnits: chars, count: length).uppercased()
|
|
}
|
|
}
|
|
return "Key\(keyCode)"
|
|
}
|
|
}
|
|
|
|
// MARK: - Serialization
|
|
|
|
func toJSON() -> String {
|
|
(try? String(data: JSONEncoder().encode(self), encoding: .utf8)) ?? "{}"
|
|
}
|
|
|
|
static func fromJSON(_ string: String) -> HotkeyBinding? {
|
|
guard let data = string.data(using: .utf8) else { return nil }
|
|
return try? JSONDecoder().decode(HotkeyBinding.self, from: data)
|
|
}
|
|
|
|
// MARK: - Presets
|
|
|
|
static let cmdReturn = HotkeyBinding(modifiers: NSEvent.ModifierFlags.command.rawValue, keyCode: 36)
|
|
static let cmdT = HotkeyBinding(modifiers: NSEvent.ModifierFlags.command.rawValue, keyCode: 17)
|
|
static let cmdW = HotkeyBinding(modifiers: NSEvent.ModifierFlags.command.rawValue, keyCode: 13)
|
|
static let cmdShiftRB = HotkeyBinding(modifiers: NSEvent.ModifierFlags([.command, .shift]).rawValue, keyCode: 30) // ]
|
|
static let cmdShiftLB = HotkeyBinding(modifiers: NSEvent.ModifierFlags([.command, .shift]).rawValue, keyCode: 33) // [
|
|
static let cmdD = HotkeyBinding(modifiers: NSEvent.ModifierFlags.command.rawValue, keyCode: 2)
|
|
}
|