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#[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#[derive(Debug, Clone, PartialEq, Eq)]
60pub enum ParseAcceleratorError {
61 IncompatibleModifier(String, String),
67 UnknownModifier(String),
69 UnknownKey(String),
71 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 fn from_str(s: &str) -> Result<Self, Self::Err> {
104 use ParseAcceleratorError as E;
105
106 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 "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 "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 "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 "`" => 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 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 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 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 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}