sql_cli/ui/key_handling/
sequence_renderer.rs

1use std::collections::VecDeque;
2use std::time::Instant;
3
4/// Represents a key sequence that might be repeated
5#[derive(Debug, Clone)]
6struct KeySequence {
7    key: String,
8    count: usize,
9    _first_press: Instant,
10    last_press: Instant,
11}
12
13/// Smart key sequence renderer that:
14/// - Collapses repeated keys (jjj -> 3j)
15/// - Limits display to last N sequences
16/// - Shows chord completions
17/// - Handles timeout/fading
18pub struct KeySequenceRenderer {
19    /// Recent key sequences
20    sequences: VecDeque<KeySequence>,
21    /// Maximum number of sequences to display
22    max_display: usize,
23    /// Time window for collapsing repeated keys (ms)
24    collapse_window_ms: u64,
25    /// Whether we're in chord mode with available completions
26    chord_mode: Option<String>,
27    /// Total fade time (ms)
28    fade_duration_ms: u64,
29    /// Enabled state
30    enabled: bool,
31}
32
33impl KeySequenceRenderer {
34    #[must_use]
35    pub fn new() -> Self {
36        Self {
37            sequences: VecDeque::with_capacity(10),
38            max_display: 5,
39            collapse_window_ms: 500, // Keys pressed within 500ms are considered "rapid"
40            chord_mode: None,
41            fade_duration_ms: 2000,
42            enabled: true, // Enable by default for better debugging
43        }
44    }
45
46    /// Enable or disable the renderer
47    pub fn set_enabled(&mut self, enabled: bool) {
48        self.enabled = enabled;
49        if !enabled {
50            self.sequences.clear();
51            self.chord_mode = None;
52        }
53    }
54
55    /// Record a key press
56    pub fn record_key(&mut self, key: String) {
57        if !self.enabled {
58            return;
59        }
60
61        let now = Instant::now();
62
63        // Check if this is a repeat of the last key
64        if let Some(last) = self.sequences.back_mut() {
65            if last.key == key
66                && last.last_press.elapsed().as_millis() < u128::from(self.collapse_window_ms)
67            {
68                // It's a rapid repeat - increment count
69                last.count += 1;
70                last.last_press = now;
71                return;
72            }
73        }
74
75        // Not a repeat, add new sequence
76        self.sequences.push_back(KeySequence {
77            key,
78            count: 1,
79            _first_press: now,
80            last_press: now,
81        });
82
83        // Clean up old sequences
84        self.cleanup_sequences();
85    }
86
87    /// Set chord mode with available completions
88    pub fn set_chord_mode(&mut self, description: Option<String>) {
89        self.chord_mode = description;
90    }
91
92    /// Clear chord mode
93    pub fn clear_chord_mode(&mut self) {
94        self.chord_mode = None;
95    }
96
97    /// Get the display string for the status line
98    #[must_use]
99    pub fn get_display(&self) -> String {
100        if !self.enabled {
101            return String::new();
102        }
103
104        // If in chord mode, show that with priority
105        if let Some(ref chord_desc) = self.chord_mode {
106            return self.format_chord_display(chord_desc);
107        }
108
109        // Otherwise show recent key sequences
110        self.format_sequence_display()
111    }
112
113    /// Format chord mode display (e.g., "y(a,c,q,v)")
114    fn format_chord_display(&self, description: &str) -> String {
115        // Parse special yank mode format
116        if description.starts_with("Yank mode:") {
117            // Extract just the key options
118            if let Some(options) = description.strip_prefix("Yank mode: ") {
119                // Convert "y=row, c=column, a=all, ESC=cancel" to "y(y,c,a,v)"
120                let keys: Vec<&str> = options
121                    .split(", ")
122                    .filter_map(|part| {
123                        let key = part.split('=').next()?;
124                        if key == "ESC" {
125                            None // Skip ESC in display
126                        } else {
127                            Some(key)
128                        }
129                    })
130                    .collect();
131
132                if !keys.is_empty() {
133                    return format!("y({})", keys.join(","));
134                }
135            }
136        }
137
138        // For other chord modes, show simplified format
139        if description.contains("Waiting for:") {
140            // Extract the waiting keys
141            if let Some(waiting) = description.strip_prefix("Waiting for: ") {
142                let parts: Vec<&str> = waiting
143                    .split(", ")
144                    .map(|p| p.split(" → ").next().unwrap_or(p))
145                    .collect();
146                if !parts.is_empty() && self.sequences.back().is_some() {
147                    if let Some(last) = self.sequences.back() {
148                        return format!("{}({})", last.key, parts.join(","));
149                    }
150                }
151            }
152        }
153
154        // Default: show the description as-is but shortened
155        if description.len() > 20 {
156            format!("{}...", &description[..17])
157        } else {
158            description.to_string()
159        }
160    }
161
162    /// Format the sequence display
163    fn format_sequence_display(&self) -> String {
164        let now = Instant::now();
165        let mut display_sequences = Vec::new();
166
167        // Collect sequences that aren't too old
168        for seq in self.sequences.iter().rev().take(self.max_display) {
169            let age_ms = now.duration_since(seq.last_press).as_millis() as u64;
170
171            // Skip if completely faded
172            if age_ms > self.fade_duration_ms {
173                continue;
174            }
175
176            // Format the sequence
177            let formatted = if seq.count > 1 {
178                // Show count for repeated keys (vim style)
179                format!("{}{}", seq.count, seq.key)
180            } else {
181                seq.key.clone()
182            };
183
184            display_sequences.push(formatted);
185        }
186
187        // Reverse to show oldest to newest (left to right)
188        display_sequences.reverse();
189
190        // Join with spaces (more compact than arrows)
191        display_sequences.join(" ")
192    }
193
194    /// Clean up old sequences
195    fn cleanup_sequences(&mut self) {
196        let now = Instant::now();
197
198        // Remove sequences older than fade duration
199        self.sequences.retain(|seq| {
200            now.duration_since(seq.last_press).as_millis() < u128::from(self.fade_duration_ms)
201        });
202
203        // Keep only last N sequences for memory efficiency
204        while self.sequences.len() > self.max_display * 2 {
205            self.sequences.pop_front();
206        }
207    }
208
209    /// Check if there's anything to display
210    #[must_use]
211    pub fn has_content(&self) -> bool {
212        self.enabled && (!self.sequences.is_empty() || self.chord_mode.is_some())
213    }
214
215    /// Clear all sequences
216    pub fn clear(&mut self) {
217        self.sequences.clear();
218        self.chord_mode = None;
219    }
220
221    /// Configure display parameters
222    pub fn configure(
223        &mut self,
224        max_display: Option<usize>,
225        collapse_window_ms: Option<u64>,
226        fade_duration_ms: Option<u64>,
227    ) {
228        if let Some(max) = max_display {
229            self.max_display = max;
230        }
231        if let Some(window) = collapse_window_ms {
232            self.collapse_window_ms = window;
233        }
234        if let Some(fade) = fade_duration_ms {
235            self.fade_duration_ms = fade;
236        }
237    }
238
239    // Debug getters for accessing internal state
240    #[must_use]
241    pub fn is_enabled(&self) -> bool {
242        self.enabled
243    }
244
245    #[must_use]
246    pub fn get_chord_mode(&self) -> &Option<String> {
247        &self.chord_mode
248    }
249
250    #[must_use]
251    pub fn sequence_count(&self) -> usize {
252        self.sequences.len()
253    }
254
255    #[must_use]
256    pub fn get_sequences(&self) -> Vec<(String, usize)> {
257        self.sequences
258            .iter()
259            .map(|seq| (seq.key.clone(), seq.count))
260            .collect()
261    }
262}
263
264impl Default for KeySequenceRenderer {
265    fn default() -> Self {
266        Self::new()
267    }
268}
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273    use std::thread::sleep;
274    use std::time::Duration;
275
276    #[test]
277    fn test_collapse_repeated_keys() {
278        let mut renderer = KeySequenceRenderer::new();
279        renderer.set_enabled(true);
280
281        // Simulate rapid j presses
282        renderer.record_key("j".to_string());
283        sleep(Duration::from_millis(50));
284        renderer.record_key("j".to_string());
285        sleep(Duration::from_millis(50));
286        renderer.record_key("j".to_string());
287
288        let display = renderer.get_display();
289        assert_eq!(display, "3j");
290    }
291
292    #[test]
293    fn test_separate_sequences() {
294        let mut renderer = KeySequenceRenderer::new();
295        renderer.set_enabled(true);
296
297        // Keys with delays between them
298        renderer.record_key("j".to_string());
299        sleep(Duration::from_millis(600)); // Longer than collapse window
300        renderer.record_key("k".to_string());
301        sleep(Duration::from_millis(600));
302        renderer.record_key("h".to_string());
303
304        let display = renderer.get_display();
305        assert_eq!(display, "j k h");
306    }
307
308    #[test]
309    fn test_chord_mode_display() {
310        let mut renderer = KeySequenceRenderer::new();
311        renderer.set_enabled(true);
312
313        renderer.record_key("y".to_string());
314        renderer.set_chord_mode(Some(
315            "Yank mode: y=row, c=column, a=all, ESC=cancel".to_string(),
316        ));
317
318        let display = renderer.get_display();
319        assert_eq!(display, "y(y,c,a)");
320    }
321
322    #[test]
323    fn test_max_display_limit() {
324        let mut renderer = KeySequenceRenderer::new();
325        renderer.set_enabled(true);
326        renderer.configure(Some(3), None, None); // Limit to 3
327
328        // Add more than limit
329        for i in 1..=10 {
330            renderer.record_key(format!("{i}"));
331            sleep(Duration::from_millis(600));
332        }
333
334        let display = renderer.get_display();
335        let parts: Vec<&str> = display.split(' ').collect();
336        assert!(parts.len() <= 3);
337    }
338
339    #[test]
340    fn test_mixed_repeated_and_single() {
341        let mut renderer = KeySequenceRenderer::new();
342        renderer.set_enabled(true);
343
344        // Mix of repeated and single keys
345        renderer.record_key("j".to_string());
346        sleep(Duration::from_millis(50));
347        renderer.record_key("j".to_string());
348        sleep(Duration::from_millis(50));
349        renderer.record_key("j".to_string());
350        sleep(Duration::from_millis(600)); // Gap
351        renderer.record_key("g".to_string());
352        sleep(Duration::from_millis(600));
353        renderer.record_key("k".to_string());
354        sleep(Duration::from_millis(50));
355        renderer.record_key("k".to_string());
356
357        let display = renderer.get_display();
358        assert_eq!(display, "3j g 2k");
359    }
360}