Sources/OrgCore/Keymap/Keys.swift
86 lines · 3432 bytes
1/// One key press in Emacs notation: `C-x`, `M-<`, `S-<left>`, `s-s`, `TAB`.
2///
3/// `C` is Control, `M` Meta, `S` Shift, `s` Super (⌘). Shift on a letter is folded into the
4/// letter (`S-a` is `A`); on other keys it stays a modifier (`S-TAB`, `S-<up>`).
5public struct KeyChord: Hashable, Sendable, CustomStringConvertible {
6 public struct Modifiers: OptionSet, Hashable, Sendable {
7 public let rawValue: Int
8 public init(rawValue: Int) { self.rawValue = rawValue }
9 public static let control = Modifiers(rawValue: 1)
10 public static let meta = Modifiers(rawValue: 2)
11 public static let shift = Modifiers(rawValue: 4)
12 public static let command = Modifiers(rawValue: 8)
13 }
14
15 /// A single character, a named key (`TAB`, `RET`, `SPC`, `ESC`, `DEL`), or a bracketed
16 /// function key (`<left>`, `<f5>`, `<delete>`).
17 public let key: String
18 public let modifiers: Modifiers
19
20 public init(_ key: String, _ modifiers: Modifiers = []) {
21 var key = Self.aliases[key] ?? key
22 var modifiers = modifiers
23 if key == "<backtab>" {
24 key = "TAB"
25 modifiers.insert(.shift)
26 }
27 if modifiers.contains(.shift), key.count == 1, let letter = key.first, letter.isLetter, letter.isLowercase {
28 key = key.uppercased()
29 modifiers.remove(.shift)
30 }
31 self.key = key
32 self.modifiers = modifiers
33 }
34
35 static let named: Set<String> = ["TAB", "RET", "SPC", "ESC", "DEL"]
36 static let aliases = [
37 " ": "SPC", "<tab>": "TAB", "<return>": "RET", "<escape>": "ESC", "<backspace>": "DEL",
38 ]
39
40 /// Parses one token such as `C-M-x`; nil if it isn't a key.
41 public static func parse<S: StringProtocol>(_ token: S) -> KeyChord? {
42 var rest = Substring(token)
43 var modifiers: Modifiers = []
44 while rest.count >= 3, rest.dropFirst().first == "-" {
45 switch rest.first {
46 case "C": modifiers.insert(.control)
47 case "M": modifiers.insert(.meta)
48 case "S": modifiers.insert(.shift)
49 case "s": modifiers.insert(.command)
50 default: return nil
51 }
52 rest = rest.dropFirst(2)
53 }
54 let key = String(rest)
55 let isFunctionKey = key.count > 2 && key.hasPrefix("<") && key.hasSuffix(">")
56 guard key.count == 1 || named.contains(key) || isFunctionKey || aliases[key] != nil else { return nil }
57 return KeyChord(key, modifiers)
58 }
59
60 public var description: String {
61 var text = ""
62 if modifiers.contains(.control) { text += "C-" }
63 if modifiers.contains(.meta) { text += "M-" }
64 if modifiers.contains(.shift) { text += "S-" }
65 if modifiers.contains(.command) { text += "s-" }
66 return text + key
67 }
68}
69
70public enum KeySequence {
71 /// Parses space-separated chords: `C-c C-x C-i`. Nil if any token isn't a key.
72 public static func parse(_ text: String) -> [KeyChord]? {
73 let tokens = text.split(separator: " ")
74 guard !tokens.isEmpty else { return nil }
75 var chords: [KeyChord] = []
76 for token in tokens {
77 guard let chord = KeyChord.parse(token) else { return nil }
78 chords.append(chord)
79 }
80 return chords
81 }
82
83 public static func format(_ keys: [KeyChord]) -> String {
84 keys.map(\.description).joined(separator: " ")
85 }
86}