sql_cli/ui/key_handling/
indicator.rs1use 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
11pub struct KeyPressIndicator {
13 key_history: VecDeque<(String, Instant)>,
15 max_keys: usize,
17 fade_start_ms: u64,
19 fade_duration_ms: u64,
21 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, fade_start_ms: 500,
31 fade_duration_ms: 1500,
32 enabled: true, }
34 }
35
36 pub fn set_enabled(&mut self, enabled: bool) {
38 self.enabled = enabled;
39 if !enabled {
40 self.key_history.clear();
41 }
42 }
43
44 pub fn record_key(&mut self, key: String) {
46 if !self.enabled {
47 return;
48 }
49
50 self.key_history.push_back((key, Instant::now()));
52
53 while self.key_history.len() > self.max_keys {
55 self.key_history.pop_front();
56 }
57
58 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 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 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 let color = self.opacity_to_color(opacity);
89
90 if i > 0 {
92 spans.push(Span::styled(" → ", Style::default().fg(Color::DarkGray)));
93 }
94
95 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 fn opacity_to_color(&self, opacity: f32) -> Color {
115 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 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
139pub 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 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 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}