sql_cli/ui/key_handling/
chord_handler.rs

1use crate::ui::input::actions::{Action, YankTarget};
2use chrono::Local;
3use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
4use std::collections::HashMap;
5use std::time::{Duration, Instant};
6use tracing::debug;
7
8/// Represents a chord sequence (e.g., "yy", "gg", "dd")
9#[derive(Debug, Clone, PartialEq, Eq, Hash)]
10pub struct ChordSequence {
11    keys: Vec<KeyEvent>,
12}
13
14impl ChordSequence {
15    #[must_use]
16    pub fn new(keys: Vec<KeyEvent>) -> Self {
17        Self { keys }
18    }
19
20    /// Create a chord from string notation like "yy" or "gg"
21    #[must_use]
22    pub fn from_notation(notation: &str) -> Option<Self> {
23        let chars: Vec<char> = notation.chars().collect();
24        if chars.is_empty() {
25            return None;
26        }
27
28        let keys: Vec<KeyEvent> = chars
29            .iter()
30            .map(|&c| KeyEvent::new(KeyCode::Char(c), KeyModifiers::empty()))
31            .collect();
32
33        Some(Self { keys })
34    }
35
36    /// Convert to human-readable string
37    pub fn to_string(&self) -> String {
38        self.keys.iter().map(format_key).collect::<String>()
39    }
40}
41
42/// Result of processing a key
43#[derive(Debug, Clone)]
44pub enum ChordResult {
45    /// No chord matched, single key press
46    SingleKey(KeyEvent),
47    /// Partial chord match, waiting for more keys
48    PartialChord(String), // Description of what we're waiting for
49    /// Complete chord matched with corresponding Action
50    CompleteChord(Action),
51    /// Chord cancelled (timeout or escape)
52    Cancelled,
53}
54
55/// Manages key chord sequences and history
56pub struct KeyChordHandler {
57    /// Map of chord sequences to Actions
58    chord_map: HashMap<ChordSequence, Action>,
59    /// Current chord being built
60    current_chord: Vec<KeyEvent>,
61    /// Time when current chord started
62    chord_start: Option<Instant>,
63    /// Timeout for chord sequences (milliseconds)
64    chord_timeout: Duration,
65    /// History of key presses for debugging
66    key_history: Vec<String>,
67    /// Maximum number of key presses to keep in history
68    max_history: usize,
69    /// Whether chord mode is active
70    chord_mode_active: bool,
71    /// Description of current chord mode (e.g., "Yank mode")
72    chord_mode_description: Option<String>,
73}
74
75impl Default for KeyChordHandler {
76    fn default() -> Self {
77        Self::new()
78    }
79}
80
81impl KeyChordHandler {
82    #[must_use]
83    pub fn new() -> Self {
84        let mut handler = Self {
85            chord_map: HashMap::new(),
86            current_chord: Vec::new(),
87            chord_start: None,
88            chord_timeout: Duration::from_millis(1000), // 1 second default
89            key_history: Vec::new(),
90            max_history: 50,
91            chord_mode_active: false,
92            chord_mode_description: None,
93        };
94        handler.setup_default_chords();
95        handler
96    }
97
98    /// Set up default chord mappings
99    fn setup_default_chords(&mut self) {
100        use crate::buffer::AppMode;
101        use crate::ui::input::actions::{CursorPosition, SqlClause};
102
103        // Yank chords - these are the only actual chords in use
104        self.register_chord_action("yy", Action::Yank(YankTarget::Row));
105        self.register_chord_action("yr", Action::Yank(YankTarget::Row)); // Alternative for yank row
106        self.register_chord_action("yc", Action::Yank(YankTarget::Column));
107        self.register_chord_action("ya", Action::Yank(YankTarget::All));
108        self.register_chord_action("yv", Action::Yank(YankTarget::Cell)); // Yank cell value
109        self.register_chord_action("yq", Action::Yank(YankTarget::Query)); // Yank current query text
110
111        // SQL clause navigation chords - jump to end of SQL clauses in command mode
112        // cw = cursor to WHERE, cs = cursor to SELECT, co = cursor to ORDER BY, etc.
113        self.register_chord_action(
114            "cw",
115            Action::SwitchModeWithCursor(
116                AppMode::Command,
117                CursorPosition::AfterClause(SqlClause::Where),
118            ),
119        );
120        self.register_chord_action(
121            "cs",
122            Action::SwitchModeWithCursor(
123                AppMode::Command,
124                CursorPosition::AfterClause(SqlClause::Select),
125            ),
126        );
127        self.register_chord_action(
128            "cf",
129            Action::SwitchModeWithCursor(
130                AppMode::Command,
131                CursorPosition::AfterClause(SqlClause::From),
132            ),
133        );
134        self.register_chord_action(
135            "co",
136            Action::SwitchModeWithCursor(
137                AppMode::Command,
138                CursorPosition::AfterClause(SqlClause::OrderBy),
139            ),
140        );
141        self.register_chord_action(
142            "cg",
143            Action::SwitchModeWithCursor(
144                AppMode::Command,
145                CursorPosition::AfterClause(SqlClause::GroupBy),
146            ),
147        );
148        self.register_chord_action(
149            "ch",
150            Action::SwitchModeWithCursor(
151                AppMode::Command,
152                CursorPosition::AfterClause(SqlClause::Having),
153            ),
154        );
155        self.register_chord_action(
156            "cl",
157            Action::SwitchModeWithCursor(
158                AppMode::Command,
159                CursorPosition::AfterClause(SqlClause::Limit),
160            ),
161        );
162
163        // Future chord possibilities (not currently implemented):
164        // self.register_chord("gg", "go_to_top");  // Currently single 'g'
165        // self.register_chord("dd", "delete_line"); // No line deletion in results
166        // self.register_chord("dw", "delete_word"); // Only in command mode with Alt+D
167    }
168
169    /// Register a chord sequence with an Action
170    pub fn register_chord_action(&mut self, notation: &str, action: Action) {
171        if let Some(chord) = ChordSequence::from_notation(notation) {
172            self.chord_map.insert(chord, action);
173        }
174    }
175
176    /// Process a key event
177    pub fn process_key(&mut self, key: KeyEvent) -> ChordResult {
178        // Log the key press
179        self.log_key_press(&key);
180
181        // Check for timeout
182        if let Some(start) = self.chord_start {
183            if start.elapsed() > self.chord_timeout {
184                self.cancel_chord();
185                // Process this key as a new sequence
186                return self.process_key_internal(key);
187            }
188        }
189
190        // Handle escape - always cancels chord
191        if key.code == KeyCode::Esc && !self.current_chord.is_empty() {
192            self.cancel_chord();
193            return ChordResult::Cancelled;
194        }
195
196        self.process_key_internal(key)
197    }
198
199    fn process_key_internal(&mut self, key: KeyEvent) -> ChordResult {
200        debug!(
201            "process_key_internal: key={:?}, current_chord={:?}",
202            key, self.current_chord
203        );
204
205        // Add key to current chord
206        self.current_chord.push(key);
207
208        // Start timer if this is the first key
209        if self.current_chord.len() == 1 {
210            self.chord_start = Some(Instant::now());
211        }
212
213        // Check for exact match
214        let current = ChordSequence::new(self.current_chord.clone());
215        debug!("Checking for exact match with chord: {:?}", current);
216        debug!(
217            "Registered chords: {:?}",
218            self.chord_map.keys().collect::<Vec<_>>()
219        );
220        if let Some(action) = self.chord_map.get(&current) {
221            debug!("Found exact match! Action: {:?}", action);
222            let result = ChordResult::CompleteChord(action.clone());
223            self.reset_chord();
224            return result;
225        }
226
227        // Check for partial matches
228        debug!("Checking for partial matches...");
229        let has_partial = self.chord_map.keys().any(|chord| {
230            chord.keys.len() > self.current_chord.len()
231                && chord.keys[..self.current_chord.len()] == self.current_chord[..]
232        });
233
234        debug!("has_partial = {}", has_partial);
235        if has_partial {
236            // Build description of possible completions
237            let possible: Vec<String> = self
238                .chord_map
239                .iter()
240                .filter_map(|(chord, action)| {
241                    if chord.keys.len() > self.current_chord.len()
242                        && chord.keys[..self.current_chord.len()] == self.current_chord[..]
243                    {
244                        let action_name = match action {
245                            Action::Yank(YankTarget::Row) => "yank row",
246                            Action::Yank(YankTarget::Column) => "yank column",
247                            Action::Yank(YankTarget::All) => "yank all",
248                            Action::Yank(YankTarget::Cell) => "yank cell",
249                            Action::Yank(YankTarget::Query) => "yank query",
250                            _ => "unknown",
251                        };
252                        Some(format!(
253                            "{} → {}",
254                            format_key(&chord.keys[self.current_chord.len()]),
255                            action_name
256                        ))
257                    } else {
258                        None
259                    }
260                })
261                .collect();
262
263            let description = if self.current_chord.len() == 1
264                && self.current_chord[0].code == KeyCode::Char('y')
265            {
266                "Yank mode: y=row, c=column, a=all, ESC=cancel".to_string()
267            } else {
268                format!("Waiting for: {}", possible.join(", "))
269            };
270
271            self.chord_mode_active = true;
272            self.chord_mode_description = Some(description.clone());
273            ChordResult::PartialChord(description)
274        } else {
275            // No match, treat as single key
276            let result = if self.current_chord.len() == 1 {
277                ChordResult::SingleKey(key)
278            } else {
279                // Multiple keys but no match - return the first as single, reset
280                ChordResult::SingleKey(self.current_chord[0])
281            };
282            self.reset_chord();
283            result
284        }
285    }
286
287    /// Cancel current chord
288    pub fn cancel_chord(&mut self) {
289        self.reset_chord();
290    }
291
292    /// Reset chord state
293    fn reset_chord(&mut self) {
294        self.current_chord.clear();
295        self.chord_start = None;
296        self.chord_mode_active = false;
297        self.chord_mode_description = None;
298    }
299
300    /// Log a key press to history
301    pub fn log_key_press(&mut self, key: &KeyEvent) {
302        if self.key_history.len() >= self.max_history {
303            self.key_history.remove(0);
304        }
305
306        let timestamp = Local::now().format("%H:%M:%S.%3f");
307        let key_str = format_key(key);
308        let modifiers = format_modifiers(key.modifiers);
309
310        let entry = if modifiers.is_empty() {
311            format!("[{timestamp}] {key_str}")
312        } else {
313            format!("[{timestamp}] {key_str} ({modifiers})")
314        };
315
316        self.key_history.push(entry);
317    }
318
319    /// Get the key press history
320    #[must_use]
321    pub fn get_history(&self) -> &[String] {
322        &self.key_history
323    }
324
325    /// Clear the key press history
326    pub fn clear_history(&mut self) {
327        self.key_history.clear();
328    }
329
330    /// Get current chord mode status
331    #[must_use]
332    pub fn is_chord_mode_active(&self) -> bool {
333        self.chord_mode_active
334    }
335
336    /// Get chord mode description
337    #[must_use]
338    pub fn get_chord_mode_description(&self) -> Option<&str> {
339        self.chord_mode_description.as_deref()
340    }
341
342    /// Set chord timeout
343    pub fn set_timeout(&mut self, millis: u64) {
344        self.chord_timeout = Duration::from_millis(millis);
345    }
346
347    /// Pretty print for debug view
348    pub fn format_debug_info(&self) -> String {
349        let mut output = String::new();
350
351        // Current chord state
352        output.push_str("========== CHORD STATE ==========\n");
353        if self.current_chord.is_empty() {
354            output.push_str("No active chord\n");
355        } else {
356            output.push_str(&format!(
357                "Current chord: {}\n",
358                self.current_chord
359                    .iter()
360                    .map(format_key)
361                    .collect::<Vec<_>>()
362                    .join(" → ")
363            ));
364            if let Some(desc) = &self.chord_mode_description {
365                output.push_str(&format!("Mode: {desc}\n"));
366            }
367            if let Some(start) = self.chord_start {
368                let elapsed = start.elapsed().as_millis();
369                let remaining = self.chord_timeout.as_millis().saturating_sub(elapsed);
370                output.push_str(&format!("Timeout in: {remaining}ms\n"));
371            }
372        }
373
374        // Registered chords
375        output.push_str("\n========== REGISTERED CHORDS ==========\n");
376        let mut chords: Vec<_> = self.chord_map.iter().collect();
377        chords.sort_by_key(|(chord, _)| chord.to_string());
378        for (chord, action) in chords {
379            let action_name = match action {
380                Action::Yank(YankTarget::Row) => "yank_row",
381                Action::Yank(YankTarget::Column) => "yank_column",
382                Action::Yank(YankTarget::All) => "yank_all",
383                Action::Yank(YankTarget::Cell) => "yank_cell",
384                Action::Yank(YankTarget::Query) => "yank_query",
385                _ => "unknown",
386            };
387            output.push_str(&format!("{} → {}\n", chord.to_string(), action_name));
388        }
389
390        // Key history
391        output.push_str("\n========== KEY PRESS HISTORY ==========\n");
392        output.push_str("(Most recent at bottom, last 50 keys)\n");
393        for entry in &self.key_history {
394            output.push_str(entry);
395            output.push('\n');
396        }
397
398        output
399    }
400
401    /// Load custom bindings from config (for future)
402    /// Note: This will need to be updated to work with Actions when config support is added
403    pub fn load_from_config(&mut self, _config: &HashMap<String, String>) {
404        // TODO: Convert string action names to Actions when loading from config
405        // for (notation, action_name) in config {
406        //     if let Some(action) = parse_action_from_string(action_name) {
407        //         self.register_chord_action(notation, action);
408        //     }
409        // }
410    }
411}
412
413/// Format a key event for display
414fn format_key(key: &KeyEvent) -> String {
415    let mut result = String::new();
416
417    // Add modifiers
418    if key.modifiers.contains(KeyModifiers::CONTROL) {
419        result.push_str("Ctrl+");
420    }
421    if key.modifiers.contains(KeyModifiers::ALT) {
422        result.push_str("Alt+");
423    }
424    if key.modifiers.contains(KeyModifiers::SHIFT) {
425        result.push_str("Shift+");
426    }
427
428    // Add key code
429    match key.code {
430        KeyCode::Char(c) => result.push(c),
431        KeyCode::Enter => result.push_str("Enter"),
432        KeyCode::Esc => result.push_str("Esc"),
433        KeyCode::Backspace => result.push_str("Backspace"),
434        KeyCode::Tab => result.push_str("Tab"),
435        KeyCode::Delete => result.push_str("Del"),
436        KeyCode::Insert => result.push_str("Ins"),
437        KeyCode::F(n) => result.push_str(&format!("F{n}")),
438        KeyCode::Left => result.push('←'),
439        KeyCode::Right => result.push('→'),
440        KeyCode::Up => result.push('↑'),
441        KeyCode::Down => result.push('↓'),
442        KeyCode::Home => result.push_str("Home"),
443        KeyCode::End => result.push_str("End"),
444        KeyCode::PageUp => result.push_str("PgUp"),
445        KeyCode::PageDown => result.push_str("PgDn"),
446        _ => result.push('?'),
447    }
448
449    result
450}
451
452/// Format modifiers for display
453fn format_modifiers(mods: KeyModifiers) -> String {
454    let mut parts = Vec::new();
455    if mods.contains(KeyModifiers::CONTROL) {
456        parts.push("Ctrl");
457    }
458    if mods.contains(KeyModifiers::ALT) {
459        parts.push("Alt");
460    }
461    if mods.contains(KeyModifiers::SHIFT) {
462        parts.push("Shift");
463    }
464    parts.join("+")
465}
466
467#[cfg(test)]
468mod tests {
469    use super::*;
470
471    #[test]
472    fn test_chord_sequence() {
473        let chord = ChordSequence::from_notation("yy").unwrap();
474        assert_eq!(chord.keys.len(), 2);
475        assert_eq!(chord.to_string(), "yy");
476    }
477
478    #[test]
479    fn test_single_key() {
480        let mut handler = KeyChordHandler::new();
481        let key = KeyEvent::new(KeyCode::Char('x'), KeyModifiers::empty());
482        match handler.process_key(key) {
483            ChordResult::SingleKey(_) => {}
484            _ => panic!("Expected single key"),
485        }
486    }
487
488    #[test]
489    fn test_chord_completion() {
490        let mut handler = KeyChordHandler::new();
491
492        // First 'y' should be partial
493        let key1 = KeyEvent::new(KeyCode::Char('y'), KeyModifiers::empty());
494        match handler.process_key(key1) {
495            ChordResult::PartialChord(_) => {}
496            _ => panic!("Expected partial chord"),
497        }
498
499        // Second 'y' should complete
500        let key2 = KeyEvent::new(KeyCode::Char('y'), KeyModifiers::empty());
501        match handler.process_key(key2) {
502            ChordResult::CompleteChord(action) => {
503                assert_eq!(action, Action::Yank(YankTarget::Row));
504            }
505            _ => panic!("Expected complete chord"),
506        }
507    }
508}