feat: configure a persistent global launcher shortcut

This commit is contained in:
2026-10-11 23:29:40 +02:00
parent 5842d86d9a
commit 7aed5c323c
7 changed files with 728 additions and 12 deletions
+133
View File
@@ -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()
}
}