rush_sync_server/input/
keyboard.rs

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