Skip to main content

aprender_test_showcase/tui/
input.rs

1//! Keyboard input handling with 100% test coverage
2//!
3//! Probar: Error prevention - Type-safe key actions prevent invalid input
4
5use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
6
7/// Actions that can be triggered by keyboard input
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum KeyAction {
10    /// Insert a character
11    InsertChar(char),
12    /// Delete character before cursor (backspace)
13    Backspace,
14    /// Delete character at cursor
15    Delete,
16    /// Move cursor left
17    CursorLeft,
18    /// Move cursor right
19    CursorRight,
20    /// Move cursor to start
21    CursorHome,
22    /// Move cursor to end
23    CursorEnd,
24    /// Evaluate the expression
25    Evaluate,
26    /// Clear the input
27    Clear,
28    /// Clear everything including history
29    ClearAll,
30    /// Recall last expression from history
31    RecallLast,
32    /// Quit the application
33    Quit,
34    /// No action (ignored input)
35    None,
36}
37
38/// Input handler that maps key events to actions
39#[derive(Debug, Default)]
40pub struct InputHandler;
41
42impl InputHandler {
43    /// Creates a new input handler
44    #[must_use]
45    pub fn new() -> Self {
46        Self
47    }
48
49    /// Maps a key event to an action
50    #[must_use]
51    pub fn handle_key(&self, event: KeyEvent) -> KeyAction {
52        let KeyEvent {
53            code, modifiers, ..
54        } = event;
55
56        // Handle Ctrl+key combinations
57        if modifiers.contains(KeyModifiers::CONTROL) {
58            return match code {
59                KeyCode::Char('c' | 'q') => KeyAction::Quit,
60                KeyCode::Char('l') => KeyAction::ClearAll,
61                KeyCode::Char('a') => KeyAction::CursorHome,
62                KeyCode::Char('e') => KeyAction::CursorEnd,
63                KeyCode::Char('u') => KeyAction::Clear,
64                _ => KeyAction::None,
65            };
66        }
67
68        // Handle regular keys
69        match code {
70            KeyCode::Char(c) => KeyAction::InsertChar(c),
71            KeyCode::Backspace => KeyAction::Backspace,
72            KeyCode::Delete => KeyAction::Delete,
73            KeyCode::Left => KeyAction::CursorLeft,
74            KeyCode::Right => KeyAction::CursorRight,
75            KeyCode::Home => KeyAction::CursorHome,
76            KeyCode::End => KeyAction::CursorEnd,
77            KeyCode::Enter => KeyAction::Evaluate,
78            KeyCode::Esc => KeyAction::Clear,
79            KeyCode::Up => KeyAction::RecallLast,
80            _ => KeyAction::None,
81        }
82    }
83
84    /// Returns true if the character is valid for calculator input
85    #[must_use]
86    pub fn is_valid_char(c: char) -> bool {
87        c.is_ascii_digit()
88            || c == '.'
89            || c == '+'
90            || c == '-'
91            || c == '*'
92            || c == '/'
93            || c == '%'
94            || c == '^'
95            || c == '('
96            || c == ')'
97            || c == ' '
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    fn key_event(code: KeyCode) -> KeyEvent {
106        KeyEvent::new(code, KeyModifiers::NONE)
107    }
108
109    fn key_event_ctrl(code: KeyCode) -> KeyEvent {
110        KeyEvent::new(code, KeyModifiers::CONTROL)
111    }
112
113    // ===== Constructor tests =====
114
115    #[test]
116    fn test_input_handler_new() {
117        let handler = InputHandler::new();
118        // Just verify it creates without panic
119        let _ = format!("{:?}", handler);
120    }
121
122    #[test]
123    fn test_input_handler_default() {
124        let handler = InputHandler;
125        let _ = format!("{:?}", handler);
126    }
127
128    // ===== Character input tests =====
129
130    #[test]
131    fn test_handle_digit_keys() {
132        let handler = InputHandler::new();
133        for c in '0'..='9' {
134            let event = key_event(KeyCode::Char(c));
135            assert_eq!(handler.handle_key(event), KeyAction::InsertChar(c));
136        }
137    }
138
139    #[test]
140    fn test_handle_operator_keys() {
141        let handler = InputHandler::new();
142        let operators = ['+', '-', '*', '/', '%', '^'];
143        for c in operators {
144            let event = key_event(KeyCode::Char(c));
145            assert_eq!(handler.handle_key(event), KeyAction::InsertChar(c));
146        }
147    }
148
149    #[test]
150    fn test_handle_parentheses() {
151        let handler = InputHandler::new();
152        assert_eq!(
153            handler.handle_key(key_event(KeyCode::Char('('))),
154            KeyAction::InsertChar('(')
155        );
156        assert_eq!(
157            handler.handle_key(key_event(KeyCode::Char(')'))),
158            KeyAction::InsertChar(')')
159        );
160    }
161
162    #[test]
163    fn test_handle_decimal_point() {
164        let handler = InputHandler::new();
165        assert_eq!(
166            handler.handle_key(key_event(KeyCode::Char('.'))),
167            KeyAction::InsertChar('.')
168        );
169    }
170
171    #[test]
172    fn test_handle_space() {
173        let handler = InputHandler::new();
174        assert_eq!(
175            handler.handle_key(key_event(KeyCode::Char(' '))),
176            KeyAction::InsertChar(' ')
177        );
178    }
179
180    // ===== Edit key tests =====
181
182    #[test]
183    fn test_handle_backspace() {
184        let handler = InputHandler::new();
185        assert_eq!(
186            handler.handle_key(key_event(KeyCode::Backspace)),
187            KeyAction::Backspace
188        );
189    }
190
191    #[test]
192    fn test_handle_delete() {
193        let handler = InputHandler::new();
194        assert_eq!(
195            handler.handle_key(key_event(KeyCode::Delete)),
196            KeyAction::Delete
197        );
198    }
199
200    // ===== Navigation key tests =====
201
202    #[test]
203    fn test_handle_left() {
204        let handler = InputHandler::new();
205        assert_eq!(
206            handler.handle_key(key_event(KeyCode::Left)),
207            KeyAction::CursorLeft
208        );
209    }
210
211    #[test]
212    fn test_handle_right() {
213        let handler = InputHandler::new();
214        assert_eq!(
215            handler.handle_key(key_event(KeyCode::Right)),
216            KeyAction::CursorRight
217        );
218    }
219
220    #[test]
221    fn test_handle_home() {
222        let handler = InputHandler::new();
223        assert_eq!(
224            handler.handle_key(key_event(KeyCode::Home)),
225            KeyAction::CursorHome
226        );
227    }
228
229    #[test]
230    fn test_handle_end() {
231        let handler = InputHandler::new();
232        assert_eq!(
233            handler.handle_key(key_event(KeyCode::End)),
234            KeyAction::CursorEnd
235        );
236    }
237
238    // ===== Action key tests =====
239
240    #[test]
241    fn test_handle_enter() {
242        let handler = InputHandler::new();
243        assert_eq!(
244            handler.handle_key(key_event(KeyCode::Enter)),
245            KeyAction::Evaluate
246        );
247    }
248
249    #[test]
250    fn test_handle_escape() {
251        let handler = InputHandler::new();
252        assert_eq!(
253            handler.handle_key(key_event(KeyCode::Esc)),
254            KeyAction::Clear
255        );
256    }
257
258    #[test]
259    fn test_handle_up() {
260        let handler = InputHandler::new();
261        assert_eq!(
262            handler.handle_key(key_event(KeyCode::Up)),
263            KeyAction::RecallLast
264        );
265    }
266
267    // ===== Ctrl key tests =====
268
269    #[test]
270    fn test_handle_ctrl_c() {
271        let handler = InputHandler::new();
272        assert_eq!(
273            handler.handle_key(key_event_ctrl(KeyCode::Char('c'))),
274            KeyAction::Quit
275        );
276    }
277
278    #[test]
279    fn test_handle_ctrl_q() {
280        let handler = InputHandler::new();
281        assert_eq!(
282            handler.handle_key(key_event_ctrl(KeyCode::Char('q'))),
283            KeyAction::Quit
284        );
285    }
286
287    #[test]
288    fn test_handle_ctrl_l() {
289        let handler = InputHandler::new();
290        assert_eq!(
291            handler.handle_key(key_event_ctrl(KeyCode::Char('l'))),
292            KeyAction::ClearAll
293        );
294    }
295
296    #[test]
297    fn test_handle_ctrl_a() {
298        let handler = InputHandler::new();
299        assert_eq!(
300            handler.handle_key(key_event_ctrl(KeyCode::Char('a'))),
301            KeyAction::CursorHome
302        );
303    }
304
305    #[test]
306    fn test_handle_ctrl_e() {
307        let handler = InputHandler::new();
308        assert_eq!(
309            handler.handle_key(key_event_ctrl(KeyCode::Char('e'))),
310            KeyAction::CursorEnd
311        );
312    }
313
314    #[test]
315    fn test_handle_ctrl_u() {
316        let handler = InputHandler::new();
317        assert_eq!(
318            handler.handle_key(key_event_ctrl(KeyCode::Char('u'))),
319            KeyAction::Clear
320        );
321    }
322
323    #[test]
324    fn test_handle_ctrl_unknown() {
325        let handler = InputHandler::new();
326        assert_eq!(
327            handler.handle_key(key_event_ctrl(KeyCode::Char('x'))),
328            KeyAction::None
329        );
330    }
331
332    // ===== Unknown key tests =====
333
334    #[test]
335    fn test_handle_unknown_key() {
336        let handler = InputHandler::new();
337        assert_eq!(
338            handler.handle_key(key_event(KeyCode::F(1))),
339            KeyAction::None
340        );
341    }
342
343    #[test]
344    fn test_handle_tab() {
345        let handler = InputHandler::new();
346        assert_eq!(handler.handle_key(key_event(KeyCode::Tab)), KeyAction::None);
347    }
348
349    // ===== Valid char tests =====
350
351    #[test]
352    fn test_is_valid_char_digits() {
353        for c in '0'..='9' {
354            assert!(InputHandler::is_valid_char(c), "Digit {c} should be valid");
355        }
356    }
357
358    #[test]
359    fn test_is_valid_char_operators() {
360        let valid = ['+', '-', '*', '/', '%', '^', '(', ')', '.', ' '];
361        for c in valid {
362            assert!(InputHandler::is_valid_char(c), "Char '{c}' should be valid");
363        }
364    }
365
366    #[test]
367    fn test_is_valid_char_invalid() {
368        let invalid = ['a', 'z', 'A', 'Z', '@', '#', '$', '!', '&', '|'];
369        for c in invalid {
370            assert!(
371                !InputHandler::is_valid_char(c),
372                "Char '{c}' should be invalid"
373            );
374        }
375    }
376
377    // ===== KeyAction tests =====
378
379    #[test]
380    fn test_key_action_debug() {
381        let action = KeyAction::Evaluate;
382        assert!(format!("{:?}", action).contains("Evaluate"));
383    }
384
385    #[test]
386    fn test_key_action_clone() {
387        let action = KeyAction::InsertChar('x');
388        let cloned = action;
389        assert_eq!(action, cloned);
390    }
391
392    #[test]
393    fn test_key_action_copy() {
394        let action = KeyAction::Quit;
395        let copied: KeyAction = action;
396        assert_eq!(action, copied);
397    }
398}