feat: configure a persistent global launcher shortcut
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
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
|
||||
private 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user