Skip to main content

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/r=row, c=column, a=all, v=cell, q=query, 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    /// Cancel a pending chord if it has exceeded the timeout.
337    ///
338    /// The normal timeout check only runs when the next key arrives, so a chord
339    /// left dangling (e.g. `y` with no follow-up key) never resets on its own.
340    /// Calling this from the idle loop lets the UI clear the chord hint once the
341    /// window lapses. Returns true if a pending chord was cleared.
342    pub fn clear_if_timed_out(&mut self) -> bool {
343        if let Some(start) = self.chord_start {
344            if start.elapsed() > self.chord_timeout {
345                self.cancel_chord();
346                return true;
347            }
348        }
349        false
350    }
351
352    /// Get chord mode description
353    #[must_use]
354    pub fn get_chord_mode_description(&self) -> Option<&str> {
355        self.chord_mode_description.as_deref()
356    }
357
358    /// Set chord timeout
359    pub fn set_timeout(&mut self, millis: u64) {
360        self.chord_timeout = Duration::from_millis(millis);
361    }
362
363    /// Pretty print for debug view
364    pub fn format_debug_info(&self) -> String {
365        let mut output = String::new();
366
367        // Current chord state
368        output.push_str("========== CHORD STATE ==========\n");
369        if self.current_chord.is_empty() {
370            output.push_str("No active chord\n");
371        } else {
372            output.push_str(&format!(
373                "Current chord: {}\n",
374                self.current_chord
375                    .iter()
376                    .map(format_key)
377                    .collect::<Vec<_>>()
378                    .join(" → ")
379            ));
380            if let Some(desc) = &self.chord_mode_description {
381                output.push_str(&format!("Mode: {desc}\n"));
382            }
383            if let Some(start) = self.chord_start {
384                let elapsed = start.elapsed().as_millis();
385                let remaining = self.chord_timeout.as_millis().saturating_sub(elapsed);
386                output.push_str(&format!("Timeout in: {remaining}ms\n"));
387            }
388        }
389
390        // Registered chords
391        output.push_str("\n========== REGISTERED CHORDS ==========\n");
392        let mut chords: Vec<_> = self.chord_map.iter().collect();
393        chords.sort_by_key(|(chord, _)| chord.to_string());
394        for (chord, action) in chords {
395            let action_name = match action {
396                Action::Yank(YankTarget::Row) => "yank_row",
397                Action::Yank(YankTarget::Column) => "yank_column",
398                Action::Yank(YankTarget::All) => "yank_all",
399                Action::Yank(YankTarget::Cell) => "yank_cell",
400                Action::Yank(YankTarget::Query) => "yank_query",
401                _ => "unknown",
402            };
403            output.push_str(&format!("{} → {}\n", chord.to_string(), action_name));
404        }
405
406        // Key history
407        output.push_str("\n========== KEY PRESS HISTORY ==========\n");
408        output.push_str("(Most recent at bottom, last 50 keys)\n");
409        for entry in &self.key_history {
410            output.push_str(entry);
411            output.push('\n');
412        }
413
414        output
415    }
416
417    /// Load custom bindings from config (for future)
418    /// Note: This will need to be updated to work with Actions when config support is added
419    pub fn load_from_config(&mut self, _config: &HashMap<String, String>) {
420        // TODO: Convert string action names to Actions when loading from config
421        // for (notation, action_name) in config {
422        //     if let Some(action) = parse_action_from_string(action_name) {
423        //         self.register_chord_action(notation, action);
424        //     }
425        // }
426    }
427}
428
429/// Format a key event for display
430fn format_key(key: &KeyEvent) -> String {
431    let mut result = String::new();
432
433    // Add modifiers
434    if key.modifiers.contains(KeyModifiers::CONTROL) {
435        result.push_str("Ctrl+");
436    }
437    if key.modifiers.contains(KeyModifiers::ALT) {
438        result.push_str("Alt+");
439    }
440    if key.modifiers.contains(KeyModifiers::SHIFT) {
441        result.push_str("Shift+");
442    }
443
444    // Add key code
445    match key.code {
446        KeyCode::Char(c) => result.push(c),
447        KeyCode::Enter => result.push_str("Enter"),
448        KeyCode::Esc => result.push_str("Esc"),
449        KeyCode::Backspace => result.push_str("Backspace"),
450        KeyCode::Tab => result.push_str("Tab"),
451        KeyCode::Delete => result.push_str("Del"),
452        KeyCode::Insert => result.push_str("Ins"),
453        KeyCode::F(n) => result.push_str(&format!("F{n}")),
454        KeyCode::Left => result.push('←'),
455        KeyCode::Right => result.push('→'),
456        KeyCode::Up => result.push('↑'),
457        KeyCode::Down => result.push('↓'),
458        KeyCode::Home => result.push_str("Home"),
459        KeyCode::End => result.push_str("End"),
460        KeyCode::PageUp => result.push_str("PgUp"),
461        KeyCode::PageDown => result.push_str("PgDn"),
462        _ => result.push('?'),
463    }
464
465    result
466}
467
468/// Format modifiers for display
469fn format_modifiers(mods: KeyModifiers) -> String {
470    let mut parts = Vec::new();
471    if mods.contains(KeyModifiers::CONTROL) {
472        parts.push("Ctrl");
473    }
474    if mods.contains(KeyModifiers::ALT) {
475        parts.push("Alt");
476    }
477    if mods.contains(KeyModifiers::SHIFT) {
478        parts.push("Shift");
479    }
480    parts.join("+")
481}
482
483#[cfg(test)]
484mod tests {
485    use super::*;
486
487    #[test]
488    fn test_chord_sequence() {
489        let chord = ChordSequence::from_notation("yy").unwrap();
490        assert_eq!(chord.keys.len(), 2);
491        assert_eq!(chord.to_string(), "yy");
492    }
493
494    #[test]
495    fn test_single_key() {
496        let mut handler = KeyChordHandler::new();
497        let key = KeyEvent::new(KeyCode::Char('x'), KeyModifiers::empty());
498        match handler.process_key(key) {
499            ChordResult::SingleKey(_) => {}
500            _ => panic!("Expected single key"),
501        }
502    }
503
504    #[test]
505    fn test_chord_completion() {
506        let mut handler = KeyChordHandler::new();
507
508        // First 'y' should be partial
509        let key1 = KeyEvent::new(KeyCode::Char('y'), KeyModifiers::empty());
510        match handler.process_key(key1) {
511            ChordResult::PartialChord(_) => {}
512            _ => panic!("Expected partial chord"),
513        }
514
515        // Second 'y' should complete
516        let key2 = KeyEvent::new(KeyCode::Char('y'), KeyModifiers::empty());
517        match handler.process_key(key2) {
518            ChordResult::CompleteChord(action) => {
519                assert_eq!(action, Action::Yank(YankTarget::Row));
520            }
521            _ => panic!("Expected complete chord"),
522        }
523    }
524}