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