134 lines
4.8 KiB
Swift
134 lines
4.8 KiB
Swift
import AppKit
|
|
import Carbon
|
|
|
|
public struct KeyboardShortcut: Equatable, Sendable {
|
|
public let keyCode: UInt16
|
|
public let modifiers: NSEvent.ModifierFlags
|
|
|
|
public init?(keyCode: UInt16, modifiers: NSEvent.ModifierFlags) {
|
|
let normalized = modifiers.intersection([.command, .option, .control, .shift])
|
|
guard !normalized.intersection([.command, .option, .control]).isEmpty,
|
|
Self.supports(keyCode) else { return nil }
|
|
self.keyCode = keyCode
|
|
self.modifiers = normalized
|
|
}
|
|
|
|
public static let commandSpace = KeyboardShortcut(keyCode: 49, modifiers: .command)!
|
|
|
|
@MainActor
|
|
public var displayName: String {
|
|
var result = ""
|
|
if modifiers.contains(.control) { result += "⌃" }
|
|
if modifiers.contains(.option) { result += "⌥" }
|
|
if modifiers.contains(.shift) { result += "⇧" }
|
|
if modifiers.contains(.command) { result += "⌘" }
|
|
return result + keyName
|
|
}
|
|
|
|
var carbonModifiers: UInt32 {
|
|
var result: UInt32 = 0
|
|
if modifiers.contains(.command) { result |= UInt32(cmdKey) }
|
|
if modifiers.contains(.option) { result |= UInt32(optionKey) }
|
|
if modifiers.contains(.control) { result |= UInt32(controlKey) }
|
|
if modifiers.contains(.shift) { result |= UInt32(shiftKey) }
|
|
return result
|
|
}
|
|
|
|
var storedValue: [String: Int] {
|
|
["keyCode": Int(keyCode), "modifiers": Int(modifiers.rawValue)]
|
|
}
|
|
|
|
init?(storedValue: Any?) {
|
|
guard let record = storedValue as? [String: Any],
|
|
let code = Self.storedInteger(record["keyCode"]),
|
|
let flags = Self.storedInteger(record["modifiers"]),
|
|
let keyCode = UInt16(exactly: code), flags >= 0,
|
|
let shortcut = Self(keyCode: keyCode, modifiers: .init(rawValue: UInt(flags))),
|
|
shortcut.modifiers.rawValue == UInt(flags) else { return nil }
|
|
self = shortcut
|
|
}
|
|
|
|
private static func storedInteger(_ value: Any?) -> Int? {
|
|
guard let number = value as? NSNumber,
|
|
CFGetTypeID(number) != CFBooleanGetTypeID() else { return nil }
|
|
return Int(exactly: number.doubleValue)
|
|
}
|
|
|
|
private static func supports(_ keyCode: UInt16) -> Bool {
|
|
switch keyCode {
|
|
case 0...51, 53, 64, 65, 67, 69, 71, 75, 76, 78...88, 90...107, 109...111, 113...126:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
var keyName: String {
|
|
switch keyCode {
|
|
case 36: return "Return"
|
|
case 48: return "Tab"
|
|
case 49: return "Space"
|
|
case 51: return "Delete"
|
|
case 53: return "Escape"
|
|
case 64: return "F17"
|
|
case 71: return "Clear"
|
|
case 76: return "Enter"
|
|
case 79: return "F18"
|
|
case 80: return "F19"
|
|
case 90: return "F20"
|
|
case 96: return "F5"
|
|
case 97: return "F6"
|
|
case 98: return "F7"
|
|
case 99: return "F3"
|
|
case 100: return "F8"
|
|
case 101: return "F9"
|
|
case 102: return "Eisu"
|
|
case 103: return "F11"
|
|
case 104: return "Kana"
|
|
case 105: return "F13"
|
|
case 106: return "F16"
|
|
case 107: return "F14"
|
|
case 109: return "F10"
|
|
case 110: return "Menu"
|
|
case 111: return "F12"
|
|
case 113: return "F15"
|
|
case 114: return "Help"
|
|
case 115: return "Home"
|
|
case 116: return "Page Up"
|
|
case 117: return "Forward Delete"
|
|
case 118: return "F4"
|
|
case 119: return "End"
|
|
case 120: return "F2"
|
|
case 121: return "Page Down"
|
|
case 122: return "F1"
|
|
case 123: return "←"
|
|
case 124: return "→"
|
|
case 125: return "↓"
|
|
case 126: return "↑"
|
|
default: return translatedKeyName ?? "Key \(keyCode)"
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private var translatedKeyName: String? {
|
|
guard let source = TISCopyCurrentKeyboardLayoutInputSource()?.takeRetainedValue(),
|
|
let property = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData) else {
|
|
return nil
|
|
}
|
|
let data = Unmanaged<CFData>.fromOpaque(property).takeUnretainedValue()
|
|
guard let bytes = CFDataGetBytePtr(data) else { return nil }
|
|
let layout = UnsafeRawPointer(bytes).assumingMemoryBound(to: UCKeyboardLayout.self)
|
|
var deadKeyState: UInt32 = 0
|
|
var length = 0
|
|
var characters = [UniChar](repeating: 0, count: 8)
|
|
let status = UCKeyTranslate(
|
|
layout, keyCode, UInt16(kUCKeyActionDisplay), 0, UInt32(LMGetKbdType()),
|
|
UInt32(kUCKeyTranslateNoDeadKeysMask), &deadKeyState,
|
|
characters.count, &length, &characters
|
|
)
|
|
guard status == noErr, length > 0 else { return nil }
|
|
return String(utf16CodeUnits: characters, count: length).uppercased()
|
|
}
|
|
}
|