sql_cli/ui/key_handling/
indicator.rs

1use ratatui::{
2    layout::Rect,
3    style::{Color, Modifier, Style},
4    text::{Line, Span},
5    widgets::{Block, Borders, Paragraph},
6    Frame,
7};
8use std::collections::VecDeque;
9use std::time::Instant;
10
11/// A visual indicator that shows recent key presses with fade effect
12pub struct KeyPressIndicator {
13    /// Recent key presses with timestamps
14    key_history: VecDeque<(String, Instant)>,
15    /// Maximum number of keys to show
16    max_keys: usize,
17    /// How long before a key starts fading (milliseconds)
18    fade_start_ms: u64,
19    /// How long the fade takes (milliseconds)
20    fade_duration_ms: u64,
21    /// Whether the indicator is enabled
22    pub enabled: bool,
23}
24
25impl KeyPressIndicator {
26    pub fn new() -> Self {
27        Self {
28            key_history: VecDeque::with_capacity(10),
29            max_keys: 10, // Allow up to 10 keys but fade will naturally limit display
30            fade_start_ms: 500,
31            fade_duration_ms: 1500,
32            enabled: true, // Enable by default for better debugging
33        }
34    }
35
36    /// Enable or disable the indicator
37    pub fn set_enabled(&mut self, enabled: bool) {
38        self.enabled = enabled;
39        if !enabled {
40            self.key_history.clear();
41        }
42    }
43
44    /// Record a key press
45    pub fn record_key(&mut self, key: String) {
46        if !self.enabled {
47            return;
48        }
49
50        // Add new key
51        self.key_history.push_back((key, Instant::now()));
52
53        // Remove old keys if we exceed capacity
54        while self.key_history.len() > self.max_keys {
55            self.key_history.pop_front();
56        }
57
58        // Remove keys that have fully faded (after fade_start + fade_duration)
59        let fade_complete = self.fade_start_ms + self.fade_duration_ms;
60        self.key_history
61            .retain(|(_, time)| time.elapsed().as_millis() < fade_complete as u128);
62    }
63
64    /// Render the indicator
65    pub fn render(&self, frame: &mut Frame, area: Rect) {
66        if !self.enabled || self.key_history.is_empty() {
67            return;
68        }
69
70        let mut spans = Vec::new();
71
72        for (i, (key, time)) in self.key_history.iter().enumerate() {
73            let elapsed_ms = time.elapsed().as_millis() as u64;
74
75            // Calculate opacity (0.0 to 1.0)
76            let opacity = if elapsed_ms < self.fade_start_ms {
77                1.0
78            } else if elapsed_ms < self.fade_start_ms + self.fade_duration_ms {
79                let fade_progress =
80                    (elapsed_ms - self.fade_start_ms) as f32 / self.fade_duration_ms as f32;
81                1.0 - fade_progress
82            } else {
83                0.0
84            };
85
86            if opacity > 0.0 {
87                // Convert opacity to color intensity
88                let color = self.opacity_to_color(opacity);
89
90                // Add separator if not first
91                if i > 0 {
92                    spans.push(Span::styled(" → ", Style::default().fg(Color::DarkGray)));
93                }
94
95                // Add the key with fading color
96                spans.push(Span::styled(
97                    key.clone(),
98                    Style::default().fg(color).add_modifier(Modifier::ITALIC),
99                ));
100            }
101        }
102
103        if !spans.is_empty() {
104            let paragraph = Paragraph::new(Line::from(spans)).block(
105                Block::default()
106                    .borders(Borders::NONE)
107                    .style(Style::default()),
108            );
109            frame.render_widget(paragraph, area);
110        }
111    }
112
113    /// Convert opacity (0.0 to 1.0) to a color
114    fn opacity_to_color(&self, opacity: f32) -> Color {
115        // Fade from bright cyan to dark gray
116        if opacity > 0.7 {
117            Color::Cyan
118        } else if opacity > 0.4 {
119            Color::Gray
120        } else {
121            Color::DarkGray
122        }
123    }
124
125    /// Create a formatted string representation for debugging
126    pub fn to_string(&self) -> String {
127        if !self.enabled || self.key_history.is_empty() {
128            return String::new();
129        }
130
131        self.key_history
132            .iter()
133            .map(|(key, _)| key.clone())
134            .collect::<Vec<_>>()
135            .join(" → ")
136    }
137}
138
139/// Format a key event for display
140pub fn format_key_for_display(key: &crossterm::event::KeyEvent) -> String {
141    use crossterm::event::{KeyCode, KeyModifiers};
142
143    let mut parts = Vec::new();
144
145    // Add modifiers
146    if key.modifiers.contains(KeyModifiers::CONTROL) {
147        parts.push("Ctrl");
148    }
149    if key.modifiers.contains(KeyModifiers::ALT) {
150        parts.push("Alt");
151    }
152    if key.modifiers.contains(KeyModifiers::SHIFT) {
153        parts.push("Shift");
154    }
155
156    // Add the key itself
157    let key_str = match key.code {
158        KeyCode::Char(c) => {
159            if key.modifiers.contains(KeyModifiers::CONTROL) {
160                c.to_uppercase().to_string()
161            } else {
162                c.to_string()
163            }
164        }
165        KeyCode::Enter => "Enter".to_string(),
166        KeyCode::Esc => "Esc".to_string(),
167        KeyCode::Backspace => "⌫".to_string(),
168        KeyCode::Tab => "Tab".to_string(),
169        KeyCode::Up => "↑".to_string(),
170        KeyCode::Down => "↓".to_string(),
171        KeyCode::Left => "←".to_string(),
172        KeyCode::Right => "→".to_string(),
173        KeyCode::Home => "Home".to_string(),
174        KeyCode::End => "End".to_string(),
175        KeyCode::PageUp => "PgUp".to_string(),
176        KeyCode::PageDown => "PgDn".to_string(),
177        KeyCode::Delete => "Del".to_string(),
178        KeyCode::F(n) => format!("F{}", n),
179        _ => "?".to_string(),
180    };
181
182    if !parts.is_empty() {
183        format!("{}-{}", parts.join("+"), key_str)
184    } else {
185        key_str
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn test_key_indicator() {
195        let mut indicator = KeyPressIndicator::new();
196        indicator.set_enabled(true);
197
198        indicator.record_key("j".to_string());
199        indicator.record_key("k".to_string());
200        indicator.record_key("Enter".to_string());
201
202        let display = indicator.to_string();
203        assert!(display.contains("j"));
204        assert!(display.contains("k"));
205        assert!(display.contains("Enter"));
206    }
207
208    #[test]
209    fn test_key_formatting() {
210        use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
211
212        let key = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
213        assert_eq!(format_key_for_display(&key), "Ctrl-C");
214
215        let key = KeyEvent::new(KeyCode::Up, KeyModifiers::empty());
216        assert_eq!(format_key_for_display(&key), "↑");
217    }
218}