krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Sources/OrgCore/Keymap/Keys.swift

15f6b0709d88971fb62ed432c3e5b8032643670a
orgstar/Sources/OrgCore/Keymap/Keys.swift history · blame · raw

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}