Created
February 8, 2023 16:18
-
-
Save BenziAhamed/40b5ea9a8b84f704c1b48f46ef36d77e to your computer and use it in GitHub Desktop.
Parsing and rendering macOS shortcuts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Cocoa | |
fileprivate let modifierMap = [ | |
"cmd": "⌘", | |
"command": "⌘", | |
"shift": "⇧", | |
"control": "^", | |
"ctrl": "^", | |
"opt": "⌥", | |
"option": "⌥", | |
"alt": "⌥", | |
"⌘": "⌘", | |
"⇧": "⇧", | |
"⌥": "⌥", | |
"^": "^", | |
] | |
fileprivate let virtualKeyMap = [ | |
"return": "↩", // kVK_Return | |
"enter": "↩", // kVK_Return | |
"ansi_enter": "⌤", // kVK_ANSI_KeypadEnter | |
"clear": "⌧", // kVK_ANSI_KeypadClear | |
"tab": "⇥", // kVK_Tab | |
"space": "␣", // kVK_Space | |
"delete": "⌫", // kVK_Delete | |
"del": "⌫", // kVK_Delete | |
"escape": "⎋", // kVK_Escape | |
"esc": "⎋", // kVK_Escape | |
"caps": "⇪", // kVK_CapsLock | |
"capslock": "⇪", // kVK_CapsLock | |
"function": "fn", // kVK_Function | |
"fn": "fn", // kVK_Function | |
"f1": "F1", // kVK_F1 | |
"f2": "F2", // kVK_F2 | |
"f3": "F3", // kVK_F3 | |
"f4": "F4", // kVK_F4 | |
"f5": "F5", // kVK_F5 | |
"f6": "F6", // kVK_F6 | |
"f7": "F7", // kVK_F7 | |
"f8": "F8", // kVK_F8 | |
"f9": "F9", // kVK_F9 | |
"f10": "F10", // kVK_F10 | |
"f11": "F11", // kVK_F11 | |
"f12": "F12", // kVK_F12 | |
"f13": "F13", // kVK_F13 | |
"f14": "F14", // kVK_F14 | |
"f15": "F15", // kVK_F15 | |
"f16": "F16", // kVK_F16 | |
"f17": "F17", // kVK_F17 | |
"f18": "F18", // kVK_F18 | |
"f19": "F19", // kVK_F19 | |
"f20": "F20", // kVK_F20 | |
"home": "↖", // kVK_Home | |
"pageup": "⇞", // kVK_PageUp | |
"forwarddelete": "⌦", // kVK_ForwardDelete | |
"fdel": "⌦", // kVK_ForwardDelete | |
"end": "↘", // kVK_End | |
"pagedown": "⇟", // kVK_PageDown | |
"left": "◀︎", // kVK_LeftArrow | |
"right": "▶︎", // kVK_RightArrow | |
"down": "▼", // kVK_DownArrow | |
"up": "▲", // kVK_UpArrow | |
"↩": "↩", // kVK_Return | |
"⌤": "⌤", // kVK_ANSI_KeypadEnter | |
"⌧": "⌧", // kVK_ANSI_KeypadClear | |
"⇥": "⇥", // kVK_Tab | |
"␣": "␣", // kVK_Space | |
"⌫": "⌫", // kVK_Delete | |
"⎋": "⎋", // kVK_Escape | |
"⇪": "⇪", // kVK_CapsLock | |
"↖": "↖", // kVK_Home | |
"⇞": "⇞", // kVK_PageUp | |
"⌦": "⌦", // kVK_ForwardDelete | |
"↘": "↘", // kVK_End | |
"⇟": "⇟", // kVK_PageDown | |
"◀︎": "◀︎", // kVK_LeftArrow | |
"▶︎": "▶︎", // kVK_RightArrow | |
"▼": "▼", // kVK_DownArrow | |
"▲": "▲", // kVK_UpArrow | |
] | |
fileprivate enum ShortcutToken { | |
case text(String) | |
case space | |
} | |
fileprivate struct ShortcutLexer { | |
static func genTokens(_ input: String) -> [ShortcutToken] { | |
var index = input.startIndex | |
var tokens = [ShortcutToken]() | |
func advance() { | |
input.formIndex(after: &index) | |
} | |
func rewind(count: Int) { | |
input.formIndex(&index, offsetBy: -count) | |
} | |
var current: Character? { | |
return index < input.endIndex ? input[index] : nil | |
} | |
func isSingleMatch(_ c: Character) -> Bool { | |
let text = "\(c)" | |
return modifierMap[text] == text || virtualKeyMap[text] == text | |
} | |
func nextToken() -> ShortcutToken? { | |
guard let c = current else { return nil } | |
switch c { | |
case let c where c == " ": | |
advance() | |
return .space | |
case let c where isSingleMatch(c): | |
advance() | |
return .text("\(c)") | |
default: | |
var text = "" | |
while let c = current, c != " ", !isSingleMatch(c) { | |
text.append(c) | |
advance() | |
} | |
return .text(text) | |
} | |
} | |
while let token = nextToken() { | |
tokens.append(token) | |
} | |
return tokens | |
} | |
} | |
ShortcutLexer.genTokens("cmd shift alt p") | |
struct Shortcut { | |
let modifiers: Set<String> | |
let virtualKey: String | |
let shortcut: String | |
private var modString: String { | |
var result = "" | |
for mod in ["^","⌥","⇧","⌘"] where modifiers.contains(mod) { | |
result.append(mod) | |
} | |
return result | |
} | |
var rendered: String { | |
var result = "" | |
result.append(modString) | |
if !virtualKey.isEmpty { | |
result.append(virtualKey) | |
} | |
else { | |
result.append(shortcut) | |
} | |
return result | |
} | |
static func from(_ text: String) -> Shortcut { | |
return ShortcutParser.genShortcut(text.lowercased()) | |
} | |
} | |
extension Shortcut : CustomDebugStringConvertible { | |
var debugDescription: String { | |
var desc = "" | |
let mods = modString | |
if !mods.isEmpty { | |
desc.append("mods(\(mods))") | |
} | |
if !virtualKey.isEmpty { | |
desc.append(desc.isEmpty ? "" : " ") | |
desc.append("vkey(\(virtualKey))") | |
} | |
else if !shortcut.isEmpty { | |
desc.append(desc.isEmpty ? "" : " ") | |
desc.append("cmd(\(shortcut))") | |
} | |
return desc | |
} | |
} | |
fileprivate struct ShortcutParser { | |
static func genShortcut(_ text: String) -> Shortcut { | |
let tokens = ShortcutLexer.genTokens(text) | |
var modifiers = Set<String>() | |
var virtualKey = "" | |
var shortcut = "" | |
for token in tokens { | |
guard case let .text(t) = token else { | |
continue | |
} | |
if let mod = modifierMap[t] { | |
modifiers.insert(mod) | |
} | |
else if let vkey = virtualKeyMap[t] { | |
virtualKey = vkey | |
} | |
else { | |
shortcut = t.uppercased() | |
} | |
} | |
return .init( | |
modifiers: modifiers, | |
virtualKey: virtualKey, | |
shortcut: shortcut | |
) | |
} | |
} | |
Shortcut.from("cmd tab a").rendered | |
Shortcut.from("cmd fn q").rendered | |
Shortcut.from("cmd fn q l s").rendered | |
Shortcut.from("cmd del 1").rendered | |
Shortcut.from("cmd space").rendered | |
Shortcut.from("ctrl space") | |
Shortcut.from("ctrl up").rendered | |
Shortcut.from("ctrl shift down").rendered | |
Shortcut.from("opt cmd shift n").rendered | |
Shortcut.from("opt cmd shift /").rendered | |
Shortcut.from("opt cmd shift f1") | |
Shortcut.from("opt cmd shift f1 q") | |
enum ShortcutMatch: Int { | |
case exact = 2 | |
case partial = 1 | |
case none = 0 | |
} | |
func match(_ shortcut1: Shortcut, _ shortcut2: Shortcut) -> ShortcutMatch { | |
let r1 = shortcut1.rendered | |
let r2 = shortcut2.rendered | |
if r1 == r2 { | |
return .exact | |
} | |
if !shortcut1.modifiers.intersection(shortcut2.modifiers).isEmpty { | |
return .partial | |
} | |
if shortcut1.virtualKey == shortcut2.virtualKey { | |
return .partial | |
} | |
if shortcut1.shortcut == shortcut2.shortcut { | |
return .partial | |
} | |
return .none | |
} | |
Shortcut.from("cmd shift q") | |
Shortcut.from("cmd shift q").rendered | |
Shortcut.from("alt shift cmd q").rendered | |
Shortcut.from("shift cmd del") | |
Shortcut.from("shift del").rendered | |
Shortcut.from("del").rendered | |
Shortcut.from("⌫").rendered | |
Shortcut.from("⌫").rendered | |
Shortcut.from("Q⌥⇧⌘").rendered == "⌥⇧⌘Q" | |
Shortcut.from("CONTROL HOME").rendered | |
Shortcut.from("CONTROL ALT SHIFT HOME").rendered | |
Shortcut.from("CONTROL ALT SHIFT CAPS").rendered | |
Shortcut.from("CONTROL ALT TAB").rendered | |
Shortcut.from("CONTROL ALT enter").rendered | |
match(Shortcut.from("cmd shift q"), Shortcut.from("cmd q")) | |
match(Shortcut.from("cmd shift q"), Shortcut.from("cmd q shift")) | |
match(Shortcut.from("cmd shift q"), Shortcut.from("q shift")) | |
match(Shortcut.from("⌫"), Shortcut.from("del")) | |
Shortcut.from("Q⌥⇧⌘") | |
Shortcut.from("⌫") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment