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