covey_schema/
hotkey.rs

1use core::fmt;
2use std::str::FromStr;
3
4use serde::{Deserialize, Serialize};
5
6#[expect(clippy::struct_excessive_bools, reason = "simpler to serialize")]
7#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
9#[serde(rename_all = "kebab-case")]
10pub struct Hotkey {
11    pub key: KeyCode,
12    #[serde(default)]
13    pub ctrl: bool,
14    #[serde(default)]
15    pub alt: bool,
16    #[serde(default)]
17    pub shift: bool,
18    #[serde(default)]
19    pub meta: bool,
20}
21
22/// A single key on a standard US QWERTY keyboard without shift being held.
23///
24/// Does **NOT** include:
25/// - Modifiers
26/// - Text editing keys like backspace / delete / insert.
27/// - Movement keys like page up / home / down arrow.
28/// - Escape or lock keys.
29/// - Media keys.
30#[rustfmt::skip]
31#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
32#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
33#[serde(rename_all = "kebab-case")]
34pub enum KeyCode {
35    Digit0, Digit1, Digit2,
36    Digit3, Digit4, Digit5,
37    Digit6, Digit7, Digit8,
38    Digit9,
39    A, B, C, D, E, F, G, H,
40    I, J, K, L, M, N, O, P,
41    Q, R, S, T, U, V, W, X,
42    Y, Z,
43    F1, F2, F3, F4,
44    F5, F6, F7, F8,
45    F9, F10, F11, F12,
46    F13, F14, F15, F16,
47    F17, F18, F19, F20,
48    F21, F22, F23, F24,
49    Backtick,
50    Hyphen, Equal,
51    Tab,
52    LeftBracket, RightBracket, Backslash,
53    Semicolon, Apostrophe, Enter,
54    Comma, Period, Slash,
55}
56
57// FromStr and Display implementations //
58
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub enum ParseAcceleratorError {
61    /// A duplicate or other incompatible modifier.
62    ///
63    /// Modifiers are incompatible if they are the same or
64    /// aliases of each other (e.g. Ctrl and Control, or
65    /// just having Alt twice).
66    IncompatibleModifier(String, String),
67    /// An unknown modifier.
68    UnknownModifier(String),
69    /// Unknown key.
70    UnknownKey(String),
71    /// Input is empty.
72    Empty,
73}
74
75impl fmt::Display for ParseAcceleratorError {
76    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77        match self {
78            Self::IncompatibleModifier(m1, m2) => write!(
79                f,
80                "incompatible modifiers {m1:?} and {m2:?}: specify only one of these"
81            ),
82            Self::UnknownModifier(m) => write!(f, "unknown modifier {m:?}"),
83            Self::UnknownKey(k) => write!(f, "unknown key {k:?}"),
84            Self::Empty => write!(f, "no accelerator provided"),
85        }
86    }
87}
88
89impl std::error::Error for ParseAcceleratorError {}
90
91impl FromStr for Hotkey {
92    type Err = ParseAcceleratorError;
93
94    /// A set of modifiers then a key code, separated by `+` characters.
95    ///
96    /// Parsing is case insensitive.
97    ///
98    /// Modifiers are one of "ctrl", "control", "alt", "shift" or "meta".
99    ///
100    /// Keys are the character produced when the key is pressed, or for
101    /// enter and tab, the strings "enter" and "tab". See [`Key`] for the
102    /// supported keys.
103    fn from_str(s: &str) -> Result<Self, Self::Err> {
104        use ParseAcceleratorError as E;
105
106        // key code should be extracted from the back
107        let mut modifiers = s.split('+');
108        let key = modifiers.next_back().ok_or(E::Empty)?;
109        let key = key
110            .parse::<KeyCode>()
111            .map_err(|ParseKeyError(s)| E::UnknownKey(s))?;
112
113        let mut ctrl = None;
114        let mut alt = None;
115        let mut shift = None;
116        let mut meta = None;
117
118        for modifier in modifiers {
119            match &*modifier.to_lowercase() {
120                "ctrl" | "control" => {
121                    if let Some(prev) = ctrl.replace(modifier) {
122                        return Err(E::IncompatibleModifier(
123                            prev.to_string(),
124                            modifier.to_string(),
125                        ));
126                    }
127                }
128                "alt" => {
129                    if let Some(prev) = alt.replace(modifier) {
130                        return Err(E::IncompatibleModifier(
131                            prev.to_string(),
132                            modifier.to_string(),
133                        ));
134                    }
135                }
136                "shift" => {
137                    if let Some(prev) = shift.replace(modifier) {
138                        return Err(E::IncompatibleModifier(
139                            prev.to_string(),
140                            modifier.to_string(),
141                        ));
142                    }
143                }
144                "meta" => {
145                    if let Some(prev) = meta.replace(modifier) {
146                        return Err(E::IncompatibleModifier(
147                            prev.to_string(),
148                            modifier.to_string(),
149                        ));
150                    }
151                }
152                _ => return Err(E::UnknownModifier(modifier.to_string())),
153            }
154        }
155
156        Ok(Self {
157            key,
158            ctrl: ctrl.is_some(),
159            alt: alt.is_some(),
160            shift: shift.is_some(),
161            meta: meta.is_some(),
162        })
163    }
164}
165
166impl fmt::Display for Hotkey {
167    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168        if self.ctrl {
169            write!(f, "Ctrl+")?;
170        }
171        if self.alt {
172            write!(f, "Alt+")?;
173        }
174        if self.shift {
175            write!(f, "Shift+")?;
176        }
177        if self.meta {
178            write!(f, "Meta+")?;
179        }
180        write!(f, "{}", self.key)
181    }
182}
183
184#[derive(Debug, Clone, PartialEq, Eq)]
185pub struct ParseKeyError(String);
186
187impl fmt::Display for ParseKeyError {
188    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189        write!(f, "unknown key {:?}", self.0)
190    }
191}
192
193impl std::error::Error for ParseKeyError {}
194
195impl FromStr for KeyCode {
196    type Err = ParseKeyError;
197
198    fn from_str(s: &str) -> Result<Self, Self::Err> {
199        #[rustfmt::skip]
200        let v = match &*s.to_lowercase() {
201            // Digits
202            "0" => Self::Digit0,
203            "1" => Self::Digit1,
204            "2" => Self::Digit2,
205            "3" => Self::Digit3,
206            "4" => Self::Digit4,
207            "5" => Self::Digit5,
208            "6" => Self::Digit6,
209            "7" => Self::Digit7,
210            "8" => Self::Digit8,
211            "9" => Self::Digit9,
212
213            // Letters
214            "a" => Self::A, "b" => Self::B, "c" => Self::C,
215            "d" => Self::D, "e" => Self::E, "f" => Self::F,
216            "g" => Self::G, "h" => Self::H, "i" => Self::I,
217            "j" => Self::J, "k" => Self::K, "l" => Self::L,
218            "m" => Self::M, "n" => Self::N, "o" => Self::O,
219            "p" => Self::P, "q" => Self::Q, "r" => Self::R,
220            "s" => Self::S, "t" => Self::T, "u" => Self::U,
221            "v" => Self::V, "w" => Self::W, "x" => Self::X,
222            "y" => Self::Y, "z" => Self::Z,
223
224            // Function keys
225            "f1" => Self::F1, "f2" => Self::F2, "f3" => Self::F3,
226            "f4" => Self::F4, "f5" => Self::F5, "f6" => Self::F6,
227            "f7" => Self::F7, "f8" => Self::F8, "f9" => Self::F9,
228            "f10" => Self::F10, "f11" => Self::F11, "f12" => Self::F12,
229            "f13" => Self::F13, "f14" => Self::F14, "f15" => Self::F15,
230            "f16" => Self::F16, "f17" => Self::F17, "f18" => Self::F18,
231            "f19" => Self::F19, "f20" => Self::F20, "f21" => Self::F21,
232            "f22" => Self::F22, "f23" => Self::F23, "f24" => Self::F24,
233
234            // Special characters
235            "`" => Self::Backtick,
236            "-" => Self::Hyphen,
237            "=" => Self::Equal,
238            "tab" => Self::Tab,
239            "[" => Self::LeftBracket,
240            "]" => Self::RightBracket,
241            "\\" => Self::Backslash,
242            ";" => Self::Semicolon,
243            "'" => Self::Apostrophe,
244            "enter" => Self::Enter,
245            "," => Self::Comma,
246            "." => Self::Period,
247            "/" => Self::Slash,
248
249            _ => return Err(ParseKeyError(s.to_string())),
250        };
251
252        Ok(v)
253    }
254}
255
256impl fmt::Display for KeyCode {
257    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258        #[rustfmt::skip]
259        let s = match self {
260            // Digits
261            KeyCode::Digit0 => "0",
262            KeyCode::Digit1 => "1",
263            KeyCode::Digit2 => "2",
264            KeyCode::Digit3 => "3",
265            KeyCode::Digit4 => "4",
266            KeyCode::Digit5 => "5",
267            KeyCode::Digit6 => "6",
268            KeyCode::Digit7 => "7",
269            KeyCode::Digit8 => "8",
270            KeyCode::Digit9 => "9",
271
272            // Letters
273            KeyCode::A => "A", KeyCode::B => "B", KeyCode::C => "C",
274            KeyCode::D => "D", KeyCode::E => "E", KeyCode::F => "F",
275            KeyCode::G => "G", KeyCode::H => "H", KeyCode::I => "I",
276            KeyCode::J => "J", KeyCode::K => "K", KeyCode::L => "L",
277            KeyCode::M => "M", KeyCode::N => "N", KeyCode::O => "O",
278            KeyCode::P => "P", KeyCode::Q => "Q", KeyCode::R => "R",
279            KeyCode::S => "S", KeyCode::T => "T", KeyCode::U => "U",
280            KeyCode::V => "V", KeyCode::W => "W", KeyCode::X => "X",
281            KeyCode::Y => "Y", KeyCode::Z => "Z",
282
283            // Function keys
284            KeyCode::F1 => "F1", KeyCode::F2 => "F2", KeyCode::F3 => "F3",
285            KeyCode::F4 => "F4", KeyCode::F5 => "F5", KeyCode::F6 => "F6",
286            KeyCode::F7 => "F7", KeyCode::F8 => "F8", KeyCode::F9 => "F9",
287            KeyCode::F10 => "F10", KeyCode::F11 => "F11", KeyCode::F12 => "F12",
288            KeyCode::F13 => "F13", KeyCode::F14 => "F14", KeyCode::F15 => "F15",
289            KeyCode::F16 => "F16", KeyCode::F17 => "F17", KeyCode::F18 => "F18",
290            KeyCode::F19 => "F19", KeyCode::F20 => "F20", KeyCode::F21 => "F21",
291            KeyCode::F22 => "F22", KeyCode::F23 => "F23", KeyCode::F24 => "F24",
292
293            // Special characters
294            KeyCode::Backtick => "`",
295            KeyCode::Hyphen => "-",
296            KeyCode::Equal => "=",
297            KeyCode::Tab => "Tab",
298            KeyCode::LeftBracket => "[",
299            KeyCode::RightBracket => "]",
300            KeyCode::Backslash => "\\",
301            KeyCode::Semicolon => ";",
302            KeyCode::Apostrophe => "'",
303            KeyCode::Enter => "Enter",
304            KeyCode::Comma => ",",
305            KeyCode::Period => ".",
306            KeyCode::Slash => "/",
307        };
308
309        f.write_str(s)
310    }
311}