Skip to main content

presentar_terminal/
input.rs

1//! Input handling for terminal applications.
2
3use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEvent, KeyModifiers};
4use presentar_core::{Event, Key, MouseButton, Point};
5
6/// Key binding configuration.
7#[derive(Debug, Clone)]
8pub struct KeyBinding {
9    /// Key code.
10    pub code: KeyCode,
11    /// Required modifiers.
12    pub modifiers: KeyModifiers,
13    /// Action name.
14    pub action: String,
15}
16
17impl KeyBinding {
18    /// Create a new key binding.
19    #[must_use]
20    pub fn new(code: KeyCode, modifiers: KeyModifiers, action: impl Into<String>) -> Self {
21        Self {
22            code,
23            modifiers,
24            action: action.into(),
25        }
26    }
27
28    /// Create a simple key binding without modifiers.
29    #[must_use]
30    pub fn simple(code: KeyCode, action: impl Into<String>) -> Self {
31        Self::new(code, KeyModifiers::NONE, action)
32    }
33
34    /// Check if this binding matches a key event.
35    #[must_use]
36    pub fn matches(&self, event: &KeyEvent) -> bool {
37        event.code == self.code && event.modifiers.contains(self.modifiers)
38    }
39}
40
41/// Input handler for converting crossterm events to presentar events.
42#[derive(Debug, Default)]
43pub struct InputHandler {
44    bindings: Vec<KeyBinding>,
45}
46
47impl InputHandler {
48    /// Create a new input handler.
49    #[must_use]
50    pub fn new() -> Self {
51        Self::default()
52    }
53
54    /// Add a key binding.
55    pub fn add_binding(&mut self, binding: KeyBinding) {
56        self.bindings.push(binding);
57    }
58
59    /// Convert a crossterm event to a presentar event.
60    #[must_use]
61    pub fn convert(&self, event: CrosstermEvent) -> Option<Event> {
62        match event {
63            CrosstermEvent::Key(key) => self.convert_key(key),
64            CrosstermEvent::Mouse(mouse) => Some(self.convert_mouse(mouse)),
65            CrosstermEvent::Resize(width, height) => Some(Event::Resize {
66                width: f32::from(width),
67                height: f32::from(height),
68            }),
69            CrosstermEvent::FocusGained => Some(Event::FocusIn),
70            CrosstermEvent::FocusLost => Some(Event::FocusOut),
71            CrosstermEvent::Paste(text) => Some(Event::TextInput { text }),
72        }
73    }
74
75    fn convert_key(&self, key: KeyEvent) -> Option<Event> {
76        let presentar_key = match key.code {
77            KeyCode::Char('a' | 'A') => Key::A,
78            KeyCode::Char('b' | 'B') => Key::B,
79            KeyCode::Char('c' | 'C') => Key::C,
80            KeyCode::Char('d' | 'D') => Key::D,
81            KeyCode::Char('e' | 'E') => Key::E,
82            KeyCode::Char('f' | 'F') => Key::F,
83            KeyCode::Char('g' | 'G') => Key::G,
84            KeyCode::Char('h' | 'H') => Key::H,
85            KeyCode::Char('i' | 'I') => Key::I,
86            KeyCode::Char('j' | 'J') => Key::J,
87            KeyCode::Char('k' | 'K') => Key::K,
88            KeyCode::Char('l' | 'L') => Key::L,
89            KeyCode::Char('m' | 'M') => Key::M,
90            KeyCode::Char('n' | 'N') => Key::N,
91            KeyCode::Char('o' | 'O') => Key::O,
92            KeyCode::Char('p' | 'P') => Key::P,
93            KeyCode::Char('q' | 'Q') => Key::Q,
94            KeyCode::Char('r' | 'R') => Key::R,
95            KeyCode::Char('s' | 'S') => Key::S,
96            KeyCode::Char('t' | 'T') => Key::T,
97            KeyCode::Char('u' | 'U') => Key::U,
98            KeyCode::Char('v' | 'V') => Key::V,
99            KeyCode::Char('w' | 'W') => Key::W,
100            KeyCode::Char('x' | 'X') => Key::X,
101            KeyCode::Char('y' | 'Y') => Key::Y,
102            KeyCode::Char('z' | 'Z') => Key::Z,
103            KeyCode::Char('0') => Key::Num0,
104            KeyCode::Char('1') => Key::Num1,
105            KeyCode::Char('2') => Key::Num2,
106            KeyCode::Char('3') => Key::Num3,
107            KeyCode::Char('4') => Key::Num4,
108            KeyCode::Char('5') => Key::Num5,
109            KeyCode::Char('6') => Key::Num6,
110            KeyCode::Char('7') => Key::Num7,
111            KeyCode::Char('8') => Key::Num8,
112            KeyCode::Char('9') => Key::Num9,
113            KeyCode::Enter => Key::Enter,
114            KeyCode::Esc => Key::Escape,
115            KeyCode::Backspace => Key::Backspace,
116            KeyCode::Tab => Key::Tab,
117            KeyCode::Delete => Key::Delete,
118            KeyCode::Insert => Key::Insert,
119            KeyCode::Up => Key::Up,
120            KeyCode::Down => Key::Down,
121            KeyCode::Left => Key::Left,
122            KeyCode::Right => Key::Right,
123            KeyCode::Home => Key::Home,
124            KeyCode::End => Key::End,
125            KeyCode::PageUp => Key::PageUp,
126            KeyCode::PageDown => Key::PageDown,
127            KeyCode::F(1) => Key::F1,
128            KeyCode::F(2) => Key::F2,
129            KeyCode::F(3) => Key::F3,
130            KeyCode::F(4) => Key::F4,
131            KeyCode::F(5) => Key::F5,
132            KeyCode::F(6) => Key::F6,
133            KeyCode::F(7) => Key::F7,
134            KeyCode::F(8) => Key::F8,
135            KeyCode::F(9) => Key::F9,
136            KeyCode::F(10) => Key::F10,
137            KeyCode::F(11) => Key::F11,
138            KeyCode::F(12) => Key::F12,
139            KeyCode::Char(' ') => Key::Space,
140            KeyCode::Char('-') => Key::Minus,
141            KeyCode::Char('=') => Key::Equal,
142            KeyCode::Char('[') => Key::BracketLeft,
143            KeyCode::Char(']') => Key::BracketRight,
144            KeyCode::Char('\\') => Key::Backslash,
145            KeyCode::Char(';') => Key::Semicolon,
146            KeyCode::Char('\'') => Key::Quote,
147            KeyCode::Char('`') => Key::Grave,
148            KeyCode::Char(',') => Key::Comma,
149            KeyCode::Char('.') => Key::Period,
150            KeyCode::Char('/') => Key::Slash,
151            // Unknown keys are ignored
152            _ => return None,
153        };
154
155        Some(Event::KeyDown { key: presentar_key })
156    }
157
158    fn convert_mouse(&self, mouse: crossterm::event::MouseEvent) -> Event {
159        use crossterm::event::{MouseButton as CtMouseButton, MouseEventKind};
160
161        let position = Point::new(f32::from(mouse.column), f32::from(mouse.row));
162
163        match mouse.kind {
164            MouseEventKind::Down(button) => Event::MouseDown {
165                position,
166                button: match button {
167                    CtMouseButton::Left => MouseButton::Left,
168                    CtMouseButton::Right => MouseButton::Right,
169                    CtMouseButton::Middle => MouseButton::Middle,
170                },
171            },
172            MouseEventKind::Up(button) => Event::MouseUp {
173                position,
174                button: match button {
175                    CtMouseButton::Left => MouseButton::Left,
176                    CtMouseButton::Right => MouseButton::Right,
177                    CtMouseButton::Middle => MouseButton::Middle,
178                },
179            },
180            MouseEventKind::Moved | MouseEventKind::Drag(_) => Event::MouseMove { position },
181            MouseEventKind::ScrollUp => Event::Scroll {
182                delta_x: 0.0,
183                delta_y: -1.0,
184            },
185            MouseEventKind::ScrollDown => Event::Scroll {
186                delta_x: 0.0,
187                delta_y: 1.0,
188            },
189            MouseEventKind::ScrollLeft => Event::Scroll {
190                delta_x: -1.0,
191                delta_y: 0.0,
192            },
193            MouseEventKind::ScrollRight => Event::Scroll {
194                delta_x: 1.0,
195                delta_y: 0.0,
196            },
197        }
198    }
199
200    /// Find a matching binding for a key event.
201    #[must_use]
202    pub fn find_binding(&self, event: &KeyEvent) -> Option<&KeyBinding> {
203        self.bindings.iter().find(|b| b.matches(event))
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210    use crossterm::event::{MouseButton as CtMouseButton, MouseEvent, MouseEventKind};
211
212    #[test]
213    fn test_key_binding_simple() {
214        let binding = KeyBinding::simple(KeyCode::Char('q'), "quit");
215        assert_eq!(binding.action, "quit");
216        assert_eq!(binding.modifiers, KeyModifiers::NONE);
217    }
218
219    #[test]
220    fn test_key_binding_with_modifiers() {
221        let binding = KeyBinding::new(KeyCode::Char('c'), KeyModifiers::CONTROL, "copy");
222        let event = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
223        assert!(binding.matches(&event));
224    }
225
226    #[test]
227    fn test_key_binding_no_match() {
228        let binding = KeyBinding::new(KeyCode::Char('c'), KeyModifiers::CONTROL, "copy");
229        let event = KeyEvent::new(KeyCode::Char('v'), KeyModifiers::CONTROL);
230        assert!(!binding.matches(&event));
231    }
232
233    #[test]
234    fn test_input_handler_add_binding() {
235        let mut handler = InputHandler::new();
236        handler.add_binding(KeyBinding::simple(KeyCode::Char('q'), "quit"));
237        assert_eq!(handler.bindings.len(), 1);
238    }
239
240    #[test]
241    fn test_input_handler_find_binding() {
242        let mut handler = InputHandler::new();
243        handler.add_binding(KeyBinding::simple(KeyCode::Char('q'), "quit"));
244        handler.add_binding(KeyBinding::new(
245            KeyCode::Char('s'),
246            KeyModifiers::CONTROL,
247            "save",
248        ));
249
250        let event = KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE);
251        let binding = handler.find_binding(&event);
252        assert!(binding.is_some());
253        assert_eq!(binding.unwrap().action, "quit");
254
255        let event2 = KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE);
256        assert!(handler.find_binding(&event2).is_none());
257    }
258
259    #[test]
260    fn test_convert_letter_keys() {
261        let handler = InputHandler::new();
262        for ch in 'a'..='z' {
263            let event = CrosstermEvent::Key(KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE));
264            let result = handler.convert(event);
265            assert!(result.is_some());
266        }
267        for ch in 'A'..='Z' {
268            let event = CrosstermEvent::Key(KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE));
269            let result = handler.convert(event);
270            assert!(result.is_some());
271        }
272    }
273
274    #[test]
275    fn test_convert_number_keys() {
276        let handler = InputHandler::new();
277        for ch in '0'..='9' {
278            let event = CrosstermEvent::Key(KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE));
279            let result = handler.convert(event);
280            assert!(result.is_some());
281        }
282    }
283
284    #[test]
285    fn test_convert_special_keys() {
286        let handler = InputHandler::new();
287        let special_keys = [
288            KeyCode::Enter,
289            KeyCode::Esc,
290            KeyCode::Backspace,
291            KeyCode::Tab,
292            KeyCode::Delete,
293            KeyCode::Insert,
294            KeyCode::Up,
295            KeyCode::Down,
296            KeyCode::Left,
297            KeyCode::Right,
298            KeyCode::Home,
299            KeyCode::End,
300            KeyCode::PageUp,
301            KeyCode::PageDown,
302        ];
303        for key in special_keys {
304            let event = CrosstermEvent::Key(KeyEvent::new(key, KeyModifiers::NONE));
305            let result = handler.convert(event);
306            assert!(result.is_some(), "Failed for {:?}", key);
307        }
308    }
309
310    #[test]
311    fn test_convert_function_keys() {
312        let handler = InputHandler::new();
313        for n in 1..=12 {
314            let event = CrosstermEvent::Key(KeyEvent::new(KeyCode::F(n), KeyModifiers::NONE));
315            let result = handler.convert(event);
316            assert!(result.is_some(), "Failed for F{}", n);
317        }
318    }
319
320    #[test]
321    fn test_convert_punctuation_keys() {
322        let handler = InputHandler::new();
323        let punct = [' ', '-', '=', '[', ']', '\\', ';', '\'', '`', ',', '.', '/'];
324        for ch in punct {
325            let event = CrosstermEvent::Key(KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE));
326            let result = handler.convert(event);
327            assert!(result.is_some(), "Failed for {:?}", ch);
328        }
329    }
330
331    #[test]
332    fn test_convert_unknown_key() {
333        let handler = InputHandler::new();
334        let event = CrosstermEvent::Key(KeyEvent::new(KeyCode::Char('£'), KeyModifiers::NONE));
335        let result = handler.convert(event);
336        assert!(result.is_none());
337    }
338
339    #[test]
340    fn test_convert_mouse_down() {
341        let handler = InputHandler::new();
342        let mouse = MouseEvent {
343            kind: MouseEventKind::Down(CtMouseButton::Left),
344            column: 10,
345            row: 5,
346            modifiers: KeyModifiers::NONE,
347        };
348        let event = CrosstermEvent::Mouse(mouse);
349        let result = handler.convert(event).unwrap();
350        assert!(
351            matches!(result, Event::MouseDown { position, button: MouseButton::Left } if position.x == 10.0 && position.y == 5.0)
352        );
353    }
354
355    #[test]
356    fn test_convert_mouse_up() {
357        let handler = InputHandler::new();
358        let mouse = MouseEvent {
359            kind: MouseEventKind::Up(CtMouseButton::Right),
360            column: 15,
361            row: 8,
362            modifiers: KeyModifiers::NONE,
363        };
364        let event = CrosstermEvent::Mouse(mouse);
365        let result = handler.convert(event).unwrap();
366        assert!(matches!(
367            result,
368            Event::MouseUp {
369                button: MouseButton::Right,
370                ..
371            }
372        ));
373    }
374
375    #[test]
376    fn test_convert_mouse_middle() {
377        let handler = InputHandler::new();
378        let mouse = MouseEvent {
379            kind: MouseEventKind::Down(CtMouseButton::Middle),
380            column: 0,
381            row: 0,
382            modifiers: KeyModifiers::NONE,
383        };
384        let event = CrosstermEvent::Mouse(mouse);
385        let result = handler.convert(event).unwrap();
386        assert!(matches!(
387            result,
388            Event::MouseDown {
389                button: MouseButton::Middle,
390                ..
391            }
392        ));
393    }
394
395    #[test]
396    fn test_convert_mouse_move() {
397        let handler = InputHandler::new();
398        let mouse = MouseEvent {
399            kind: MouseEventKind::Moved,
400            column: 20,
401            row: 10,
402            modifiers: KeyModifiers::NONE,
403        };
404        let event = CrosstermEvent::Mouse(mouse);
405        let result = handler.convert(event).unwrap();
406        assert!(matches!(result, Event::MouseMove { position } if position.x == 20.0));
407    }
408
409    #[test]
410    fn test_convert_mouse_drag() {
411        let handler = InputHandler::new();
412        let mouse = MouseEvent {
413            kind: MouseEventKind::Drag(CtMouseButton::Left),
414            column: 25,
415            row: 12,
416            modifiers: KeyModifiers::NONE,
417        };
418        let event = CrosstermEvent::Mouse(mouse);
419        let result = handler.convert(event).unwrap();
420        assert!(matches!(result, Event::MouseMove { .. }));
421    }
422
423    #[test]
424    fn test_convert_scroll_events() {
425        let handler = InputHandler::new();
426
427        let scroll_up = MouseEvent {
428            kind: MouseEventKind::ScrollUp,
429            column: 0,
430            row: 0,
431            modifiers: KeyModifiers::NONE,
432        };
433        let result = handler.convert(CrosstermEvent::Mouse(scroll_up)).unwrap();
434        assert!(matches!(result, Event::Scroll { delta_y, .. } if delta_y < 0.0));
435
436        let scroll_down = MouseEvent {
437            kind: MouseEventKind::ScrollDown,
438            column: 0,
439            row: 0,
440            modifiers: KeyModifiers::NONE,
441        };
442        let result = handler.convert(CrosstermEvent::Mouse(scroll_down)).unwrap();
443        assert!(matches!(result, Event::Scroll { delta_y, .. } if delta_y > 0.0));
444
445        let scroll_left = MouseEvent {
446            kind: MouseEventKind::ScrollLeft,
447            column: 0,
448            row: 0,
449            modifiers: KeyModifiers::NONE,
450        };
451        let result = handler.convert(CrosstermEvent::Mouse(scroll_left)).unwrap();
452        assert!(matches!(result, Event::Scroll { delta_x, .. } if delta_x < 0.0));
453
454        let scroll_right = MouseEvent {
455            kind: MouseEventKind::ScrollRight,
456            column: 0,
457            row: 0,
458            modifiers: KeyModifiers::NONE,
459        };
460        let result = handler
461            .convert(CrosstermEvent::Mouse(scroll_right))
462            .unwrap();
463        assert!(matches!(result, Event::Scroll { delta_x, .. } if delta_x > 0.0));
464    }
465
466    #[test]
467    fn test_convert_resize() {
468        let handler = InputHandler::new();
469        let event = CrosstermEvent::Resize(120, 40);
470        let result = handler.convert(event).unwrap();
471        assert!(
472            matches!(result, Event::Resize { width, height } if width == 120.0 && height == 40.0)
473        );
474    }
475
476    #[test]
477    fn test_convert_focus_events() {
478        let handler = InputHandler::new();
479
480        let result = handler.convert(CrosstermEvent::FocusGained).unwrap();
481        assert!(matches!(result, Event::FocusIn));
482
483        let result = handler.convert(CrosstermEvent::FocusLost).unwrap();
484        assert!(matches!(result, Event::FocusOut));
485    }
486
487    #[test]
488    fn test_convert_paste() {
489        let handler = InputHandler::new();
490        let event = CrosstermEvent::Paste("hello world".to_string());
491        let result = handler.convert(event).unwrap();
492        assert!(matches!(result, Event::TextInput { text } if text == "hello world"));
493    }
494}