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