sql_cli/ui/key_handling/
sequence_renderer.rs1use std::collections::VecDeque;
2use std::time::Instant;
3
4#[derive(Debug, Clone)]
6struct KeySequence {
7 key: String,
8 count: usize,
9 first_press: Instant,
10 last_press: Instant,
11}
12
13pub struct KeySequenceRenderer {
19 sequences: VecDeque<KeySequence>,
21 max_display: usize,
23 collapse_window_ms: u64,
25 chord_mode: Option<String>,
27 fade_duration_ms: u64,
29 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, chord_mode: None,
41 fade_duration_ms: 2000,
42 enabled: true, }
44 }
45
46 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 pub fn record_key(&mut self, key: String) {
57 if !self.enabled {
58 return;
59 }
60
61 let now = Instant::now();
62
63 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 last.count += 1;
70 last.last_press = now;
71 return;
72 }
73 }
74
75 self.sequences.push_back(KeySequence {
77 key,
78 count: 1,
79 first_press: now,
80 last_press: now,
81 });
82
83 self.cleanup_sequences();
85 }
86
87 pub fn set_chord_mode(&mut self, description: Option<String>) {
89 self.chord_mode = description;
90 }
91
92 pub fn clear_chord_mode(&mut self) {
94 self.chord_mode = None;
95 }
96
97 #[must_use]
99 pub fn get_display(&self) -> String {
100 if !self.enabled {
101 return String::new();
102 }
103
104 if let Some(ref chord_desc) = self.chord_mode {
106 return self.format_chord_display(chord_desc);
107 }
108
109 self.format_sequence_display()
111 }
112
113 fn format_chord_display(&self, description: &str) -> String {
115 if description.starts_with("Yank mode:") {
117 if let Some(options) = description.strip_prefix("Yank mode: ") {
119 let keys: Vec<&str> = options
121 .split(", ")
122 .filter_map(|part| {
123 let key = part.split('=').next()?;
124 if key == "ESC" {
125 None } 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 if description.contains("Waiting for:") {
140 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 if description.len() > 20 {
156 format!("{}...", &description[..17])
157 } else {
158 description.to_string()
159 }
160 }
161
162 fn format_sequence_display(&self) -> String {
164 let now = Instant::now();
165 let mut display_sequences = Vec::new();
166
167 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 if age_ms > self.fade_duration_ms {
173 continue;
174 }
175
176 let formatted = if seq.count > 1 {
178 format!("{}{}", seq.count, seq.key)
180 } else {
181 seq.key.clone()
182 };
183
184 display_sequences.push(formatted);
185 }
186
187 display_sequences.reverse();
189
190 display_sequences.join(" ")
192 }
193
194 fn cleanup_sequences(&mut self) {
196 let now = Instant::now();
197
198 self.sequences.retain(|seq| {
200 now.duration_since(seq.last_press).as_millis() < u128::from(self.fade_duration_ms)
201 });
202
203 while self.sequences.len() > self.max_display * 2 {
205 self.sequences.pop_front();
206 }
207 }
208
209 #[must_use]
211 pub fn has_content(&self) -> bool {
212 self.enabled && (!self.sequences.is_empty() || self.chord_mode.is_some())
213 }
214
215 pub fn clear(&mut self) {
217 self.sequences.clear();
218 self.chord_mode = None;
219 }
220
221 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 #[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 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 renderer.record_key("j".to_string());
299 sleep(Duration::from_millis(600)); 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); 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 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)); 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}