ricecoder_keybinds/
models.rs

1//! Core data models for keybinds
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5use std::str::FromStr;
6
7use crate::error::ParseError;
8
9/// Represents a keyboard modifier (Ctrl, Shift, Alt, Meta)
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
11#[serde(rename_all = "lowercase")]
12pub enum Modifier {
13    Ctrl,
14    Shift,
15    Alt,
16    Meta,
17}
18
19impl fmt::Display for Modifier {
20    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21        match self {
22            Modifier::Ctrl => write!(f, "Ctrl"),
23            Modifier::Shift => write!(f, "Shift"),
24            Modifier::Alt => write!(f, "Alt"),
25            Modifier::Meta => write!(f, "Meta"),
26        }
27    }
28}
29
30impl FromStr for Modifier {
31    type Err = ParseError;
32
33    fn from_str(s: &str) -> Result<Self, Self::Err> {
34        match s.to_lowercase().as_str() {
35            "ctrl" | "control" => Ok(Modifier::Ctrl),
36            "shift" => Ok(Modifier::Shift),
37            "alt" => Ok(Modifier::Alt),
38            "meta" | "cmd" | "command" => Ok(Modifier::Meta),
39            _ => Err(ParseError::InvalidModifier(s.to_string())),
40        }
41    }
42}
43
44/// Represents a key on the keyboard
45#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
46#[serde(untagged)]
47pub enum Key {
48    Char(char),
49    F(u8),
50    Enter,
51    Escape,
52    Tab,
53    Backspace,
54    Delete,
55    Home,
56    End,
57    PageUp,
58    PageDown,
59    Up,
60    Down,
61    Left,
62    Right,
63}
64
65impl fmt::Display for Key {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        match self {
68            Key::Char(c) => write!(f, "{}", c),
69            Key::F(n) => write!(f, "F{}", n),
70            Key::Enter => write!(f, "Enter"),
71            Key::Escape => write!(f, "Escape"),
72            Key::Tab => write!(f, "Tab"),
73            Key::Backspace => write!(f, "Backspace"),
74            Key::Delete => write!(f, "Delete"),
75            Key::Home => write!(f, "Home"),
76            Key::End => write!(f, "End"),
77            Key::PageUp => write!(f, "PageUp"),
78            Key::PageDown => write!(f, "PageDown"),
79            Key::Up => write!(f, "Up"),
80            Key::Down => write!(f, "Down"),
81            Key::Left => write!(f, "Left"),
82            Key::Right => write!(f, "Right"),
83        }
84    }
85}
86
87impl FromStr for Key {
88    type Err = ParseError;
89
90    fn from_str(s: &str) -> Result<Self, Self::Err> {
91        match s.to_lowercase().as_str() {
92            "enter" | "return" => Ok(Key::Enter),
93            "escape" | "esc" => Ok(Key::Escape),
94            "tab" => Ok(Key::Tab),
95            "backspace" | "bksp" => Ok(Key::Backspace),
96            "delete" | "del" => Ok(Key::Delete),
97            "home" => Ok(Key::Home),
98            "end" => Ok(Key::End),
99            "pageup" | "page_up" => Ok(Key::PageUp),
100            "pagedown" | "page_down" => Ok(Key::PageDown),
101            "up" => Ok(Key::Up),
102            "down" => Ok(Key::Down),
103            "left" => Ok(Key::Left),
104            "right" => Ok(Key::Right),
105            s if s.starts_with('f') && s.len() > 1 => {
106                let num: u8 = s[1..].parse().map_err(|_| {
107                    ParseError::InvalidKeySyntax(format!("Invalid function key: {}", s))
108                })?;
109                if (1..=12).contains(&num) {
110                    Ok(Key::F(num))
111                } else {
112                    Err(ParseError::InvalidKeySyntax(format!(
113                        "Function key must be F1-F12, got: {}",
114                        s
115                    )))
116                }
117            }
118            s if s.len() == 1 => Ok(Key::Char(s.chars().next().unwrap())),
119            _ => Err(ParseError::InvalidKeySyntax(format!(
120                "Unknown key: {}",
121                s
122            ))),
123        }
124    }
125}
126
127/// Represents a key combination (modifiers + key)
128#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
129pub struct KeyCombo {
130    pub modifiers: Vec<Modifier>,
131    pub key: Key,
132}
133
134impl fmt::Display for KeyCombo {
135    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136        for modifier in &self.modifiers {
137            write!(f, "{}+", modifier)?;
138        }
139        write!(f, "{}", self.key)
140    }
141}
142
143impl FromStr for KeyCombo {
144    type Err = ParseError;
145
146    fn from_str(s: &str) -> Result<Self, Self::Err> {
147        let parts: Vec<&str> = s.split('+').collect();
148        if parts.is_empty() {
149            return Err(ParseError::InvalidKeySyntax(
150                "Empty key combination".to_string(),
151            ));
152        }
153
154        let mut modifiers = Vec::new();
155        for part in &parts[..parts.len() - 1] {
156            modifiers.push(Modifier::from_str(part)?);
157        }
158
159        let key = Key::from_str(parts[parts.len() - 1])?;
160
161        Ok(KeyCombo { modifiers, key })
162    }
163}
164
165/// Represents a single keybind configuration
166#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
167pub struct Keybind {
168    pub action_id: String,
169    pub key: String,
170    pub category: String,
171    pub description: String,
172    #[serde(default)]
173    pub is_default: bool,
174}
175
176impl Keybind {
177    /// Parse the key string into a KeyCombo
178    pub fn parse_key(&self) -> Result<KeyCombo, ParseError> {
179        KeyCombo::from_str(&self.key)
180    }
181
182    /// Create a new keybind
183    pub fn new(
184        action_id: impl Into<String>,
185        key: impl Into<String>,
186        category: impl Into<String>,
187        description: impl Into<String>,
188    ) -> Self {
189        Keybind {
190            action_id: action_id.into(),
191            key: key.into(),
192            category: category.into(),
193            description: description.into(),
194            is_default: false,
195        }
196    }
197
198    /// Create a new default keybind
199    pub fn new_default(
200        action_id: impl Into<String>,
201        key: impl Into<String>,
202        category: impl Into<String>,
203        description: impl Into<String>,
204    ) -> Self {
205        Keybind {
206            action_id: action_id.into(),
207            key: key.into(),
208            category: category.into(),
209            description: description.into(),
210            is_default: true,
211        }
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    #[test]
220    fn test_modifier_from_str() {
221        assert_eq!(Modifier::from_str("ctrl").unwrap(), Modifier::Ctrl);
222        assert_eq!(Modifier::from_str("Shift").unwrap(), Modifier::Shift);
223        assert_eq!(Modifier::from_str("alt").unwrap(), Modifier::Alt);
224        assert_eq!(Modifier::from_str("meta").unwrap(), Modifier::Meta);
225        assert_eq!(Modifier::from_str("cmd").unwrap(), Modifier::Meta);
226    }
227
228    #[test]
229    fn test_key_from_str() {
230        assert_eq!(Key::from_str("enter").unwrap(), Key::Enter);
231        assert_eq!(Key::from_str("F1").unwrap(), Key::F(1));
232        assert_eq!(Key::from_str("a").unwrap(), Key::Char('a'));
233        assert!(Key::from_str("F13").is_err());
234    }
235
236    #[test]
237    fn test_key_combo_from_str() {
238        let combo = KeyCombo::from_str("Ctrl+S").unwrap();
239        assert_eq!(combo.modifiers.len(), 1);
240        assert_eq!(combo.modifiers[0], Modifier::Ctrl);
241        assert_eq!(combo.key, Key::Char('s'));
242
243        let combo = KeyCombo::from_str("Ctrl+Shift+Z").unwrap();
244        assert_eq!(combo.modifiers.len(), 2);
245        assert_eq!(combo.key, Key::Char('z'));
246    }
247
248    #[test]
249    fn test_keybind_creation() {
250        let kb = Keybind::new("editor.save", "Ctrl+S", "editing", "Save file");
251        assert_eq!(kb.action_id, "editor.save");
252        assert_eq!(kb.key, "Ctrl+S");
253        assert!(!kb.is_default);
254
255        let kb = Keybind::new_default("editor.undo", "Ctrl+Z", "editing", "Undo");
256        assert!(kb.is_default);
257    }
258}