Skip to main content

hotkey_listener/
hotkey.rs

1//! Hotkey definition with optional modifiers.
2
3use crate::key::Key;
4use anyhow::{anyhow, Result};
5
6/// Modifier keys that can be combined with a hotkey.
7#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
8pub struct Modifiers {
9    pub shift: bool,
10    pub ctrl: bool,
11    pub alt: bool,
12}
13
14/// A hotkey consisting of a key and optional modifiers.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct Hotkey {
17    pub key: Key,
18    pub modifiers: Modifiers,
19}
20
21impl Hotkey {
22    /// Create a new hotkey with no modifiers.
23    pub fn new(key: Key) -> Self {
24        Self {
25            key,
26            modifiers: Modifiers::default(),
27        }
28    }
29
30    /// Create a new hotkey with the given modifiers.
31    pub fn with_modifiers(key: Key, modifiers: Modifiers) -> Self {
32        Self { key, modifiers }
33    }
34
35    /// Return a copy of this hotkey with the shift modifier added.
36    pub fn with_shift(&self) -> Self {
37        Self {
38            key: self.key,
39            modifiers: Modifiers {
40                shift: true,
41                ctrl: self.modifiers.ctrl,
42                alt: self.modifiers.alt,
43            },
44        }
45    }
46}
47
48impl std::fmt::Display for Hotkey {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        let mut parts = Vec::new();
51        if self.modifiers.ctrl {
52            parts.push("Ctrl".to_string());
53        }
54        if self.modifiers.alt {
55            parts.push("Alt".to_string());
56        }
57        if self.modifiers.shift {
58            parts.push("Shift".to_string());
59        }
60        parts.push(self.key.to_string());
61        write!(f, "{}", parts.join("+"))
62    }
63}
64
65/// Parse a hotkey string like "Shift+F8" or "F10" into a Hotkey.
66pub fn parse_hotkey(s: &str) -> Result<Hotkey> {
67    let parts: Vec<&str> = s.split('+').collect();
68    let mut modifiers = Modifiers::default();
69
70    if parts.is_empty() {
71        return Err(anyhow!("Empty hotkey string"));
72    }
73
74    // Parse modifiers (all parts except the last one)
75    for part in &parts[..parts.len() - 1] {
76        match part.to_uppercase().as_str() {
77            "SHIFT" => modifiers.shift = true,
78            "CTRL" | "CONTROL" => modifiers.ctrl = true,
79            "ALT" => modifiers.alt = true,
80            _ => return Err(anyhow!("Unknown modifier: {}", part)),
81        }
82    }
83
84    // Parse the key (last part)
85    let key_str = parts[parts.len() - 1];
86    let key = Key::parse(key_str)?;
87
88    Ok(Hotkey { key, modifiers })
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn test_parse_simple_key() {
97        let hotkey = parse_hotkey("F8").unwrap();
98        assert_eq!(hotkey.key, Key::F8);
99        assert!(!hotkey.modifiers.shift);
100        assert!(!hotkey.modifiers.ctrl);
101        assert!(!hotkey.modifiers.alt);
102    }
103
104    #[test]
105    fn test_parse_with_shift() {
106        let hotkey = parse_hotkey("Shift+F8").unwrap();
107        assert_eq!(hotkey.key, Key::F8);
108        assert!(hotkey.modifiers.shift);
109        assert!(!hotkey.modifiers.ctrl);
110        assert!(!hotkey.modifiers.alt);
111    }
112
113    #[test]
114    fn test_parse_with_multiple_modifiers() {
115        let hotkey = parse_hotkey("Ctrl+Alt+F1").unwrap();
116        assert_eq!(hotkey.key, Key::F1);
117        assert!(!hotkey.modifiers.shift);
118        assert!(hotkey.modifiers.ctrl);
119        assert!(hotkey.modifiers.alt);
120    }
121
122    #[test]
123    fn test_parse_case_insensitive() {
124        let hotkey = parse_hotkey("SHIFT+f8").unwrap();
125        assert_eq!(hotkey.key, Key::F8);
126        assert!(hotkey.modifiers.shift);
127    }
128
129    #[test]
130    fn test_parse_unknown_key() {
131        assert!(parse_hotkey("Unknown").is_err());
132    }
133
134    #[test]
135    fn test_parse_unknown_modifier() {
136        assert!(parse_hotkey("Meta+F8").is_err());
137    }
138
139    #[test]
140    fn test_display() {
141        let hotkey = parse_hotkey("Shift+F8").unwrap();
142        assert_eq!(hotkey.to_string(), "Shift+F8");
143    }
144}