Skip to main content

rush_sync_server/input/
keyboard.rs

1use crate::core::constants::DOUBLE_ESC_THRESHOLD;
2use crate::core::prelude::*;
3use crossterm::event::KeyModifiers;
4use std::sync::{LazyLock, Mutex};
5
6#[derive(Debug, Clone, PartialEq)]
7pub enum KeyAction {
8    MoveLeft,
9    MoveRight,
10    MoveToStart,
11    MoveToEnd,
12    InsertChar(char),
13    Backspace,
14    Delete,
15    Submit,
16    Cancel,
17    Quit,
18    ClearLine,
19    CopySelection,
20    PasteBuffer,
21    NoAction,
22    ScrollUp,
23    ScrollDown,
24    PageUp,
25    PageDown,
26}
27
28static LAST_ESC_PRESS: LazyLock<Mutex<Option<Instant>>> = LazyLock::new(|| Mutex::new(None));
29static ESCAPE_SEQUENCE_BUFFER: LazyLock<Mutex<Vec<char>>> =
30    LazyLock::new(|| Mutex::new(Vec::new()));
31
32pub struct KeyboardManager {
33    double_press_threshold: Duration,
34    sequence_timeout: Duration,
35    last_key_time: Instant,
36}
37
38impl KeyboardManager {
39    pub fn new() -> Self {
40        Self {
41            double_press_threshold: Duration::from_millis(DOUBLE_ESC_THRESHOLD),
42            sequence_timeout: Duration::from_millis(100),
43            last_key_time: Instant::now(),
44        }
45    }
46
47    // Consolidated security filtering
48    fn is_safe_char(&mut self, c: char) -> bool {
49        // Filter dangerous control chars and sequences
50        if matches!(c, '\x00'..='\x08' | '\x0B'..='\x0C' | '\x0E'..='\x1F' | '\x7F') {
51            return false;
52        }
53
54        // Filter suspicious non-ASCII chars (except common European chars)
55        if !c.is_ascii() && !c.is_alphabetic() && !"äöüßÄÖÜ€".contains(c) {
56            return false;
57        }
58
59        // Check for terminal sequence patterns
60        !self.detect_terminal_sequence(c)
61    }
62
63    fn detect_terminal_sequence(&mut self, c: char) -> bool {
64        let now = Instant::now();
65
66        // Reset old buffer
67        if now.duration_since(self.last_key_time) > self.sequence_timeout {
68            if let Ok(mut buffer) = ESCAPE_SEQUENCE_BUFFER.lock() {
69                buffer.clear();
70            }
71        }
72        self.last_key_time = now;
73
74        // Check sequences
75        if let Ok(mut buffer) = ESCAPE_SEQUENCE_BUFFER.lock() {
76            buffer.push(c);
77            let sequence: String = buffer.iter().collect();
78
79            // Detect dangerous patterns
80            let is_dangerous = sequence.to_lowercase().contains("tmux")
81                || (sequence.len() > 3
82                    && sequence.chars().all(|ch| ch.is_ascii_digit() || ch == ';'))
83                || sequence.contains("///")
84                || sequence.contains(";;;");
85
86            if is_dangerous {
87                buffer.clear();
88            }
89            if buffer.len() > 20 {
90                buffer.drain(0..10);
91            }
92
93            is_dangerous
94        } else {
95            false
96        }
97    }
98
99    pub fn get_action(&mut self, key: &KeyEvent) -> KeyAction {
100        // Handle ESC double-press
101        if key.code == KeyCode::Esc {
102            return self.handle_escape();
103        }
104
105        // Filter dangerous characters
106        if let KeyCode::Char(c) = key.code {
107            if !self.is_safe_char(c) {
108                return KeyAction::NoAction;
109            }
110        }
111
112        // Quick scroll detection
113        if key.modifiers.contains(KeyModifiers::SHIFT) {
114            match key.code {
115                KeyCode::Up => return KeyAction::ScrollUp,
116                KeyCode::Down => return KeyAction::ScrollDown,
117                _ => {}
118            }
119        }
120
121        // Main key mapping - consolidated
122        match (key.code, key.modifiers) {
123            // Basic movement
124            (KeyCode::Left, KeyModifiers::NONE) => KeyAction::MoveLeft,
125            (KeyCode::Right, KeyModifiers::NONE) => KeyAction::MoveRight,
126            (KeyCode::Home, KeyModifiers::NONE) => KeyAction::MoveToStart,
127            (KeyCode::End, KeyModifiers::NONE) => KeyAction::MoveToEnd,
128            (KeyCode::Enter, KeyModifiers::NONE) => KeyAction::Submit,
129
130            // Scrolling
131            (KeyCode::PageUp, KeyModifiers::NONE) => KeyAction::PageUp,
132            (KeyCode::PageDown, KeyModifiers::NONE) => KeyAction::PageDown,
133
134            // Text editing
135            (KeyCode::Backspace, KeyModifiers::NONE) => KeyAction::Backspace,
136            (KeyCode::Delete, KeyModifiers::NONE) => KeyAction::Delete,
137
138            // Platform-specific shortcuts - consolidated
139            (KeyCode::Char(c), mods) => self.handle_char_with_modifiers(c, mods),
140
141            // Arrow keys with modifiers
142            (KeyCode::Left, mods) if self.is_move_modifier(mods) => KeyAction::MoveToStart,
143            (KeyCode::Right, mods) if self.is_move_modifier(mods) => KeyAction::MoveToEnd,
144
145            // Backspace with modifiers
146            (KeyCode::Backspace, mods) if self.is_clear_modifier(mods) => KeyAction::ClearLine,
147
148            _ => KeyAction::NoAction,
149        }
150    }
151
152    fn handle_escape(&self) -> KeyAction {
153        let now = Instant::now();
154        let mut last_press = LAST_ESC_PRESS.lock().unwrap_or_else(|p| p.into_inner());
155
156        if let Some(prev_press) = *last_press {
157            if now.duration_since(prev_press) <= self.double_press_threshold {
158                *last_press = None;
159                return KeyAction::Quit;
160            }
161        }
162
163        *last_press = Some(now);
164        KeyAction::NoAction
165    }
166
167    fn handle_char_with_modifiers(&self, c: char, mods: KeyModifiers) -> KeyAction {
168        // Safe character input (no modifiers or just shift)
169        if mods.is_empty() || mods == KeyModifiers::SHIFT {
170            return if c.is_ascii_control() && c != '\t' {
171                KeyAction::NoAction
172            } else {
173                KeyAction::InsertChar(c)
174            };
175        }
176
177        // Shortcut handling - consolidated for all platforms
178        match c {
179            'c' if self.is_copy_modifier(mods) => KeyAction::CopySelection,
180            'v' if self.is_paste_modifier(mods) => KeyAction::PasteBuffer,
181            'x' if self.is_cut_modifier(mods) => KeyAction::ClearLine,
182            'a' if self.is_select_modifier(mods) => KeyAction::MoveToStart,
183            'e' if self.is_end_modifier(mods) => KeyAction::MoveToEnd,
184            'u' if self.is_clear_modifier(mods) => KeyAction::ClearLine,
185            _ => KeyAction::NoAction,
186        }
187    }
188
189    // Platform-agnostic modifier checks
190    fn is_copy_modifier(&self, mods: KeyModifiers) -> bool {
191        mods.contains(KeyModifiers::SUPER)
192            || mods.contains(KeyModifiers::CONTROL)
193            || mods.contains(KeyModifiers::ALT)
194    }
195
196    fn is_paste_modifier(&self, mods: KeyModifiers) -> bool {
197        self.is_copy_modifier(mods)
198    }
199    fn is_cut_modifier(&self, mods: KeyModifiers) -> bool {
200        self.is_copy_modifier(mods)
201    }
202    fn is_select_modifier(&self, mods: KeyModifiers) -> bool {
203        self.is_copy_modifier(mods)
204    }
205    fn is_end_modifier(&self, mods: KeyModifiers) -> bool {
206        mods.contains(KeyModifiers::CONTROL) || mods.contains(KeyModifiers::ALT)
207    }
208    fn is_clear_modifier(&self, mods: KeyModifiers) -> bool {
209        self.is_copy_modifier(mods)
210    }
211    fn is_move_modifier(&self, mods: KeyModifiers) -> bool {
212        self.is_copy_modifier(mods)
213    }
214}
215
216impl Default for KeyboardManager {
217    fn default() -> Self {
218        Self::new()
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225    use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
226
227    #[test]
228    fn test_escape_sequence_filtering() {
229        let mut manager = KeyboardManager::new();
230
231        // Test dangerous control character
232        let ctrl_char = KeyEvent::new(KeyCode::Char('\x1B'), KeyModifiers::NONE);
233        assert_eq!(manager.get_action(&ctrl_char), KeyAction::NoAction);
234
235        // Test safe character
236        let normal_char = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
237        assert_eq!(manager.get_action(&normal_char), KeyAction::InsertChar('a'));
238    }
239
240    #[test]
241    fn test_platform_shortcuts() {
242        let mut manager = KeyboardManager::new();
243
244        // Test CMD shortcuts (Mac)
245        let cmd_c = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::SUPER);
246        assert_eq!(manager.get_action(&cmd_c), KeyAction::CopySelection);
247
248        // Test CTRL shortcuts (Windows/Linux)
249        let ctrl_c = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
250        assert_eq!(manager.get_action(&ctrl_c), KeyAction::CopySelection);
251
252        // Test ALT shortcuts (fallback)
253        let alt_c = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::ALT);
254        assert_eq!(manager.get_action(&alt_c), KeyAction::CopySelection);
255    }
256
257    #[test]
258    fn test_scroll_actions() {
259        let mut manager = KeyboardManager::new();
260
261        let shift_up = KeyEvent::new(KeyCode::Up, KeyModifiers::SHIFT);
262        assert_eq!(manager.get_action(&shift_up), KeyAction::ScrollUp);
263
264        let shift_down = KeyEvent::new(KeyCode::Down, KeyModifiers::SHIFT);
265        assert_eq!(manager.get_action(&shift_down), KeyAction::ScrollDown);
266    }
267
268    #[test]
269    fn test_double_escape() {
270        let mut manager = KeyboardManager::new();
271        let esc_key = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
272
273        // First ESC should return NoAction
274        assert_eq!(manager.get_action(&esc_key), KeyAction::NoAction);
275
276        // Quick second ESC should return Quit (if within threshold)
277        // Note: This test is simplified - in real usage, timing matters
278    }
279}