1use bitflags::bitflags;
4use serde::{Deserialize, Serialize};
5use std::fmt;
6use std::str::FromStr;
7
8use crate::error::{Error, Result};
9
10bitflags! {
11 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
16 #[serde(transparent)]
17 pub struct Modifiers: u32 {
18 const CMD_LEFT = 1 << 0;
20 const SHIFT_LEFT = 1 << 1;
21 const CTRL_LEFT = 1 << 2;
22 const OPT_LEFT = 1 << 3;
23 const FN = 1 << 4;
24 const CMD_RIGHT = 1 << 5;
25 const SHIFT_RIGHT = 1 << 6;
26 const CTRL_RIGHT = 1 << 7;
27 const OPT_RIGHT = 1 << 8;
28
29 const CMD = Self::CMD_LEFT.bits() | Self::CMD_RIGHT.bits();
31 const SHIFT = Self::SHIFT_LEFT.bits() | Self::SHIFT_RIGHT.bits();
32 const CTRL = Self::CTRL_LEFT.bits() | Self::CTRL_RIGHT.bits();
33 const OPT = Self::OPT_LEFT.bits() | Self::OPT_RIGHT.bits();
34 }
35}
36
37const GROUPS: [(Modifiers, Modifiers, Modifiers); 4] = [
39 (Modifiers::CMD_LEFT, Modifiers::CMD_RIGHT, Modifiers::CMD),
40 (
41 Modifiers::SHIFT_LEFT,
42 Modifiers::SHIFT_RIGHT,
43 Modifiers::SHIFT,
44 ),
45 (Modifiers::CTRL_LEFT, Modifiers::CTRL_RIGHT, Modifiers::CTRL),
46 (Modifiers::OPT_LEFT, Modifiers::OPT_RIGHT, Modifiers::OPT),
47];
48
49impl fmt::Display for Modifiers {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 let mut parts = Vec::new();
52
53 if self.contains(Modifiers::CTRL) {
55 parts.push("Ctrl");
56 } else if self.contains(Modifiers::CTRL_LEFT) {
57 parts.push("CtrlLeft");
58 } else if self.contains(Modifiers::CTRL_RIGHT) {
59 parts.push("CtrlRight");
60 }
61
62 if self.contains(Modifiers::OPT) {
64 parts.push("Opt");
65 } else if self.contains(Modifiers::OPT_LEFT) {
66 parts.push("OptLeft");
67 } else if self.contains(Modifiers::OPT_RIGHT) {
68 parts.push("OptRight");
69 }
70
71 if self.contains(Modifiers::SHIFT) {
73 parts.push("Shift");
74 } else if self.contains(Modifiers::SHIFT_LEFT) {
75 parts.push("ShiftLeft");
76 } else if self.contains(Modifiers::SHIFT_RIGHT) {
77 parts.push("ShiftRight");
78 }
79
80 if self.contains(Modifiers::CMD) {
82 parts.push("Cmd");
83 } else if self.contains(Modifiers::CMD_LEFT) {
84 parts.push("CmdLeft");
85 } else if self.contains(Modifiers::CMD_RIGHT) {
86 parts.push("CmdRight");
87 }
88
89 if self.contains(Modifiers::FN) {
91 parts.push("Fn");
92 }
93
94 write!(f, "{}", parts.join("+"))
95 }
96}
97
98impl Modifiers {
99 pub(crate) fn parse_single(s: &str) -> Option<Modifiers> {
101 match s.to_lowercase().as_str() {
102 "cmd" | "command" | "meta" | "super" | "win" | "windows" => Some(Modifiers::CMD),
104 "shift" => Some(Modifiers::SHIFT),
105 "ctrl" | "control" => Some(Modifiers::CTRL),
106 "opt" | "option" | "alt" => Some(Modifiers::OPT),
107 "fn" | "function" => Some(Modifiers::FN),
108
109 "cmdleft" | "cmd_left" | "lcmd" | "commandleft" | "command_left" | "lcommand"
111 | "superleft" | "super_left" | "winleft" | "win_left" | "windowsleft"
112 | "windows_left" | "metaleft" | "meta_left" => Some(Modifiers::CMD_LEFT),
113 "shiftleft" | "shift_left" | "lshift" => Some(Modifiers::SHIFT_LEFT),
114 "ctrlleft" | "ctrl_left" | "lctrl" | "controlleft" | "control_left" | "lcontrol" => {
115 Some(Modifiers::CTRL_LEFT)
116 }
117 "optleft" | "opt_left" | "lopt" | "optionleft" | "option_left" | "loption"
118 | "altleft" | "alt_left" | "lalt" => Some(Modifiers::OPT_LEFT),
119
120 "cmdright" | "cmd_right" | "rcmd" | "commandright" | "command_right" | "rcommand"
122 | "superright" | "super_right" | "winright" | "win_right" | "windowsright"
123 | "windows_right" | "metaright" | "meta_right" => Some(Modifiers::CMD_RIGHT),
124 "shiftright" | "shift_right" | "rshift" => Some(Modifiers::SHIFT_RIGHT),
125 "ctrlright" | "ctrl_right" | "rctrl" | "controlright" | "control_right"
126 | "rcontrol" => Some(Modifiers::CTRL_RIGHT),
127 "optright" | "opt_right" | "ropt" | "optionright" | "option_right" | "roption"
128 | "altright" | "alt_right" | "ralt" | "altgr" => Some(Modifiers::OPT_RIGHT),
129
130 _ => None,
131 }
132 }
133
134 pub fn matches(self, event: Modifiers) -> bool {
143 for &(left, right, _compound) in &GROUPS {
144 let hotkey_has_left = self.contains(left);
145 let hotkey_has_right = self.contains(right);
146 let event_has_left = event.contains(left);
147 let event_has_right = event.contains(right);
148 let event_has_any = event_has_left || event_has_right;
149
150 if hotkey_has_left && hotkey_has_right {
151 if !event_has_any {
153 return false;
154 }
155 } else if hotkey_has_left {
156 if !event_has_left {
158 return false;
159 }
160 } else if hotkey_has_right {
161 if !event_has_right {
163 return false;
164 }
165 } else {
166 if event_has_any {
168 return false;
169 }
170 }
171 }
172
173 self.contains(Modifiers::FN) == event.contains(Modifiers::FN)
175 }
176}
177
178impl FromStr for Modifiers {
179 type Err = Error;
180
181 fn from_str(s: &str) -> Result<Self> {
183 let s = s.trim();
184 if s.is_empty() {
185 return Ok(Modifiers::empty());
186 }
187
188 let mut modifiers = Modifiers::empty();
189 for part in s.split('+') {
190 let part = part.trim();
191 if part.is_empty() {
192 continue;
193 }
194 match Modifiers::parse_single(part) {
195 Some(m) => modifiers |= m,
196 None => return Err(Error::UnknownModifier(part.to_string())),
197 }
198 }
199 Ok(modifiers)
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[test]
208 fn parse_single_modifiers() {
209 assert_eq!("Cmd".parse::<Modifiers>().unwrap(), Modifiers::CMD);
210 assert_eq!("command".parse::<Modifiers>().unwrap(), Modifiers::CMD);
211 assert_eq!("meta".parse::<Modifiers>().unwrap(), Modifiers::CMD);
212 assert_eq!("super".parse::<Modifiers>().unwrap(), Modifiers::CMD);
213 assert_eq!("win".parse::<Modifiers>().unwrap(), Modifiers::CMD);
214
215 assert_eq!("Shift".parse::<Modifiers>().unwrap(), Modifiers::SHIFT);
216 assert_eq!("SHIFT".parse::<Modifiers>().unwrap(), Modifiers::SHIFT);
217
218 assert_eq!("Ctrl".parse::<Modifiers>().unwrap(), Modifiers::CTRL);
219 assert_eq!("control".parse::<Modifiers>().unwrap(), Modifiers::CTRL);
220
221 assert_eq!("Opt".parse::<Modifiers>().unwrap(), Modifiers::OPT);
222 assert_eq!("option".parse::<Modifiers>().unwrap(), Modifiers::OPT);
223 assert_eq!("alt".parse::<Modifiers>().unwrap(), Modifiers::OPT);
224
225 assert_eq!("Fn".parse::<Modifiers>().unwrap(), Modifiers::FN);
226 assert_eq!("function".parse::<Modifiers>().unwrap(), Modifiers::FN);
227 }
228
229 #[test]
230 fn parse_side_specific_modifiers() {
231 assert_eq!("CmdLeft".parse::<Modifiers>().unwrap(), Modifiers::CMD_LEFT);
232 assert_eq!("LCmd".parse::<Modifiers>().unwrap(), Modifiers::CMD_LEFT);
233 assert_eq!(
234 "CmdRight".parse::<Modifiers>().unwrap(),
235 Modifiers::CMD_RIGHT
236 );
237 assert_eq!("RCmd".parse::<Modifiers>().unwrap(), Modifiers::CMD_RIGHT);
238
239 assert_eq!(
240 "ShiftLeft".parse::<Modifiers>().unwrap(),
241 Modifiers::SHIFT_LEFT
242 );
243 assert_eq!(
244 "ShiftRight".parse::<Modifiers>().unwrap(),
245 Modifiers::SHIFT_RIGHT
246 );
247
248 assert_eq!(
249 "CtrlLeft".parse::<Modifiers>().unwrap(),
250 Modifiers::CTRL_LEFT
251 );
252 assert_eq!(
253 "CtrlRight".parse::<Modifiers>().unwrap(),
254 Modifiers::CTRL_RIGHT
255 );
256
257 assert_eq!("OptLeft".parse::<Modifiers>().unwrap(), Modifiers::OPT_LEFT);
258 assert_eq!(
259 "AltRight".parse::<Modifiers>().unwrap(),
260 Modifiers::OPT_RIGHT
261 );
262 assert_eq!("AltGr".parse::<Modifiers>().unwrap(), Modifiers::OPT_RIGHT);
263 }
264
265 #[test]
266 fn parse_combined_modifiers() {
267 assert_eq!(
268 "Cmd+Shift".parse::<Modifiers>().unwrap(),
269 Modifiers::CMD | Modifiers::SHIFT
270 );
271 assert_eq!(
272 "Ctrl+Alt+Shift".parse::<Modifiers>().unwrap(),
273 Modifiers::CTRL | Modifiers::OPT | Modifiers::SHIFT
274 );
275 }
276
277 #[test]
278 fn parse_empty_modifiers() {
279 assert_eq!("".parse::<Modifiers>().unwrap(), Modifiers::empty());
280 assert_eq!(" ".parse::<Modifiers>().unwrap(), Modifiers::empty());
281 }
282
283 #[test]
284 fn parse_unknown_modifier_fails() {
285 assert!("Unknown".parse::<Modifiers>().is_err());
286 assert!("Cmd+Unknown".parse::<Modifiers>().is_err());
287 }
288
289 #[test]
290 fn modifiers_display() {
291 assert_eq!(format!("{}", Modifiers::CMD), "Cmd");
292 assert_eq!(format!("{}", Modifiers::SHIFT), "Shift");
293 assert_eq!(
294 format!("{}", Modifiers::CMD | Modifiers::SHIFT),
295 "Shift+Cmd"
296 );
297 }
298
299 #[test]
300 fn modifiers_display_side_specific() {
301 assert_eq!(format!("{}", Modifiers::CMD_LEFT), "CmdLeft");
302 assert_eq!(format!("{}", Modifiers::CMD_RIGHT), "CmdRight");
303 assert_eq!(format!("{}", Modifiers::SHIFT_LEFT), "ShiftLeft");
304 assert_eq!(format!("{}", Modifiers::CTRL_RIGHT), "CtrlRight");
305 assert_eq!(format!("{}", Modifiers::OPT_LEFT), "OptLeft");
306 }
307
308 #[test]
309 fn matches_compound_hotkey() {
310 let hotkey = Modifiers::CMD;
312 assert!(hotkey.matches(Modifiers::CMD_LEFT));
313 assert!(hotkey.matches(Modifiers::CMD_RIGHT));
314 assert!(hotkey.matches(Modifiers::CMD_LEFT | Modifiers::CMD_RIGHT));
315 assert!(!hotkey.matches(Modifiers::empty()));
316 assert!(!hotkey.matches(Modifiers::SHIFT_LEFT));
317 }
318
319 #[test]
320 fn matches_side_specific_hotkey() {
321 let hotkey = Modifiers::CMD_LEFT;
323 assert!(hotkey.matches(Modifiers::CMD_LEFT));
324 assert!(!hotkey.matches(Modifiers::CMD_RIGHT));
325 assert!(hotkey.matches(Modifiers::CMD_LEFT | Modifiers::CMD_RIGHT));
327 assert!(!hotkey.matches(Modifiers::empty()));
328 }
329
330 #[test]
331 fn matches_rejects_extra_groups() {
332 let hotkey = Modifiers::CMD;
334 assert!(!hotkey.matches(Modifiers::CMD_LEFT | Modifiers::SHIFT_LEFT));
335
336 let hotkey = Modifiers::CMD_LEFT | Modifiers::SHIFT_LEFT;
338 assert!(hotkey.matches(Modifiers::CMD_LEFT | Modifiers::SHIFT_LEFT));
339 }
340
341 #[test]
342 fn matches_fn_exact() {
343 let hotkey = Modifiers::CMD | Modifiers::FN;
344 assert!(hotkey.matches(Modifiers::CMD_LEFT | Modifiers::FN));
345 assert!(!hotkey.matches(Modifiers::CMD_LEFT)); let hotkey = Modifiers::CMD;
348 assert!(!hotkey.matches(Modifiers::CMD_LEFT | Modifiers::FN)); }
350
351 #[test]
352 fn matches_empty() {
353 let hotkey = Modifiers::empty();
354 assert!(hotkey.matches(Modifiers::empty()));
355 assert!(!hotkey.matches(Modifiers::CMD_LEFT));
356 }
357
358 #[test]
359 fn compound_equals_both_sides() {
360 assert_eq!(Modifiers::CMD, Modifiers::CMD_LEFT | Modifiers::CMD_RIGHT);
361 assert_eq!(
362 Modifiers::SHIFT,
363 Modifiers::SHIFT_LEFT | Modifiers::SHIFT_RIGHT
364 );
365 assert_eq!(
366 Modifiers::CTRL,
367 Modifiers::CTRL_LEFT | Modifiers::CTRL_RIGHT
368 );
369 assert_eq!(Modifiers::OPT, Modifiers::OPT_LEFT | Modifiers::OPT_RIGHT);
370 }
371}