|
| 1 | +import Carbon |
| 2 | +import Foundation |
| 3 | + |
| 4 | +public struct KeyShortcut: Equatable { |
| 5 | + public let keyCode: UInt32 |
| 6 | + public let modifiers: UInt32 |
| 7 | + |
| 8 | + public init(keyCode: UInt32, modifiers: UInt32) { |
| 9 | + self.keyCode = keyCode |
| 10 | + self.modifiers = modifiers |
| 11 | + } |
| 12 | +} |
| 13 | + |
| 14 | +public enum ShortcutParser { |
| 15 | + public static func parse(_ rawShortcut: String) -> KeyShortcut? { |
| 16 | + let shortcut = rawShortcut.trimmingCharacters(in: .whitespacesAndNewlines) |
| 17 | + guard !shortcut.isEmpty else { |
| 18 | + return nil |
| 19 | + } |
| 20 | + |
| 21 | + let modifierTokens: [String] |
| 22 | + let keyToken: String |
| 23 | + |
| 24 | + if shortcut.contains("<") { |
| 25 | + let regex = try? NSRegularExpression(pattern: "<([^>]+)>") |
| 26 | + let nsRange = NSRange(shortcut.startIndex..<shortcut.endIndex, in: shortcut) |
| 27 | + let matches = regex?.matches(in: shortcut, range: nsRange) ?? [] |
| 28 | + |
| 29 | + modifierTokens = matches.compactMap { match in |
| 30 | + guard let range = Range(match.range(at: 1), in: shortcut) else { return nil } |
| 31 | + return String(shortcut[range]).lowercased() |
| 32 | + } |
| 33 | + |
| 34 | + keyToken = |
| 35 | + regex?.stringByReplacingMatches(in: shortcut, range: nsRange, withTemplate: "") |
| 36 | + .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" |
| 37 | + } else if shortcut.contains("+") { |
| 38 | + let parts = shortcut.split(separator: "+").map { |
| 39 | + String($0).trimmingCharacters(in: .whitespacesAndNewlines) |
| 40 | + } |
| 41 | + guard let last = parts.last else { |
| 42 | + return nil |
| 43 | + } |
| 44 | + |
| 45 | + modifierTokens = parts.dropLast().map { $0.lowercased() } |
| 46 | + keyToken = last |
| 47 | + } else { |
| 48 | + modifierTokens = [] |
| 49 | + keyToken = shortcut |
| 50 | + } |
| 51 | + |
| 52 | + guard let keyCode = keyCodeForToken(keyToken) else { |
| 53 | + return nil |
| 54 | + } |
| 55 | + |
| 56 | + var modifiers: UInt32 = 0 |
| 57 | + for token in modifierTokens { |
| 58 | + switch token { |
| 59 | + case "control", "ctrl", "primary": |
| 60 | + modifiers |= UInt32(controlKey) |
| 61 | + case "option", "alt", "mod1": |
| 62 | + modifiers |= UInt32(optionKey) |
| 63 | + case "shift": |
| 64 | + modifiers |= UInt32(shiftKey) |
| 65 | + case "command", "cmd", "super", "meta", "mod4": |
| 66 | + modifiers |= UInt32(cmdKey) |
| 67 | + default: |
| 68 | + break |
| 69 | + } |
| 70 | + } |
| 71 | + |
| 72 | + return KeyShortcut(keyCode: keyCode, modifiers: modifiers) |
| 73 | + } |
| 74 | + |
| 75 | + private static func keyCodeForToken(_ rawToken: String) -> UInt32? { |
| 76 | + let token = rawToken.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() |
| 77 | + guard !token.isEmpty else { |
| 78 | + return nil |
| 79 | + } |
| 80 | + |
| 81 | + let keyMap: [String: UInt32] = [ |
| 82 | + "a": UInt32(kVK_ANSI_A), "b": UInt32(kVK_ANSI_B), "c": UInt32(kVK_ANSI_C), |
| 83 | + "d": UInt32(kVK_ANSI_D), "e": UInt32(kVK_ANSI_E), "f": UInt32(kVK_ANSI_F), |
| 84 | + "g": UInt32(kVK_ANSI_G), "h": UInt32(kVK_ANSI_H), "i": UInt32(kVK_ANSI_I), |
| 85 | + "j": UInt32(kVK_ANSI_J), "k": UInt32(kVK_ANSI_K), "l": UInt32(kVK_ANSI_L), |
| 86 | + "m": UInt32(kVK_ANSI_M), "n": UInt32(kVK_ANSI_N), "o": UInt32(kVK_ANSI_O), |
| 87 | + "p": UInt32(kVK_ANSI_P), "q": UInt32(kVK_ANSI_Q), "r": UInt32(kVK_ANSI_R), |
| 88 | + "s": UInt32(kVK_ANSI_S), "t": UInt32(kVK_ANSI_T), "u": UInt32(kVK_ANSI_U), |
| 89 | + "v": UInt32(kVK_ANSI_V), "w": UInt32(kVK_ANSI_W), "x": UInt32(kVK_ANSI_X), |
| 90 | + "y": UInt32(kVK_ANSI_Y), "z": UInt32(kVK_ANSI_Z), |
| 91 | + "0": UInt32(kVK_ANSI_0), "1": UInt32(kVK_ANSI_1), "2": UInt32(kVK_ANSI_2), |
| 92 | + "3": UInt32(kVK_ANSI_3), "4": UInt32(kVK_ANSI_4), "5": UInt32(kVK_ANSI_5), |
| 93 | + "6": UInt32(kVK_ANSI_6), "7": UInt32(kVK_ANSI_7), "8": UInt32(kVK_ANSI_8), |
| 94 | + "9": UInt32(kVK_ANSI_9), |
| 95 | + "space": UInt32(kVK_Space), |
| 96 | + "f1": UInt32(kVK_F1), "f2": UInt32(kVK_F2), "f3": UInt32(kVK_F3), |
| 97 | + "f4": UInt32(kVK_F4), "f5": UInt32(kVK_F5), "f6": UInt32(kVK_F6), |
| 98 | + "f7": UInt32(kVK_F7), "f8": UInt32(kVK_F8), "f9": UInt32(kVK_F9), |
| 99 | + "f10": UInt32(kVK_F10), "f11": UInt32(kVK_F11), "f12": UInt32(kVK_F12), |
| 100 | + ] |
| 101 | + |
| 102 | + return keyMap[token] |
| 103 | + } |
| 104 | +} |
0 commit comments