1use ratatui::{
2 buffer::Buffer,
3 layout::{Constraint, Direction, Layout, Rect},
4 style::{Color, Style},
5 widgets::{Block, Paragraph, Widget},
6};
7
8pub struct Controls {
9 pub row_count: Option<usize>,
10 pub dimmed: bool,
11 pub query_active: bool,
12 pub custom_controls: Option<Vec<(&'static str, &'static str)>>,
13 pub bg_color: Color,
14 pub key_color: Color, pub label_color: Color, pub throbber_color: Color,
17 pub use_unicode_throbber: bool, pub busy: bool, pub throbber_frame: u8, }
21
22impl Default for Controls {
23 fn default() -> Self {
24 Self {
25 row_count: None,
26 dimmed: false,
27 query_active: false,
28 custom_controls: None,
29 bg_color: Color::Indexed(236), key_color: Color::Cyan, label_color: Color::White, throbber_color: Color::Cyan,
33 use_unicode_throbber: false,
34 busy: false,
35 throbber_frame: 0,
36 }
37 }
38}
39
40impl Controls {
41 pub fn new() -> Self {
42 Self::default()
43 }
44
45 pub fn with_row_count(row_count: usize) -> Self {
46 Self {
47 row_count: Some(row_count),
48 dimmed: false,
49 query_active: false,
50 custom_controls: None,
51 bg_color: Color::Indexed(236), key_color: Color::Cyan, label_color: Color::White, throbber_color: Color::Cyan,
55 use_unicode_throbber: false,
56 busy: false,
57 throbber_frame: 0,
58 }
59 }
60
61 pub fn with_busy(mut self, busy: bool, throbber_frame: u8) -> Self {
62 self.busy = busy;
63 self.throbber_frame = throbber_frame;
64 self
65 }
66
67 pub fn with_dimmed(mut self, dimmed: bool) -> Self {
68 self.dimmed = dimmed;
69 self
70 }
71
72 pub fn with_query_active(mut self, query_active: bool) -> Self {
73 self.query_active = query_active;
74 self
75 }
76
77 pub fn with_custom_controls(mut self, controls: Vec<(&'static str, &'static str)>) -> Self {
78 self.custom_controls = Some(controls);
79 self
80 }
81
82 pub fn with_colors(
83 mut self,
84 bg_color: Color,
85 key_color: Color,
86 label_color: Color,
87 throbber_color: Color,
88 ) -> Self {
89 self.bg_color = bg_color;
90 self.key_color = key_color;
91 self.label_color = label_color;
92 self.throbber_color = throbber_color;
93 self
94 }
95
96 pub fn with_unicode_throbber(mut self, use_unicode: bool) -> Self {
97 self.use_unicode_throbber = use_unicode;
98 self
99 }
100
101 pub fn with_row_count_and_colors(
102 row_count: usize,
103 bg_color: Color,
104 key_color: Color,
105 label_color: Color,
106 throbber_color: Color,
107 ) -> Self {
108 Self {
109 row_count: Some(row_count),
110 dimmed: false,
111 query_active: false,
112 custom_controls: None,
113 bg_color,
114 key_color,
115 label_color,
116 throbber_color,
117 use_unicode_throbber: false,
118 busy: false,
119 throbber_frame: 0,
120 }
121 }
122}
123
124impl Widget for &Controls {
125 fn render(self, area: Rect, buf: &mut Buffer) {
126 let no_bg = self.bg_color == Color::Reset;
127 if !no_bg {
128 Block::default()
129 .style(Style::default().bg(self.bg_color))
130 .render(area, buf);
131 }
132
133 const DEFAULT_CONTROLS: [(&str, &str); 9] = [
134 ("/", "Query"),
135 ("i", "Info"),
136 ("a", "Analysis"),
137 ("c", "Chart"),
138 ("s", "Sort & Filter"),
139 ("p", "Pivot & Melt"),
140 ("e", "Export"),
141 ("?", "Help"),
142 ("q", "Quit"),
143 ];
144
145 let controls: Vec<(&str, &str)> = if let Some(ref custom) = self.custom_controls {
146 custom.to_vec()
147 } else {
148 DEFAULT_CONTROLS.to_vec()
149 };
150
151 let pair_width = |(key, action): &(&str, &str)| -> u16 {
154 (key.chars().count() as u16 + 1) + (action.chars().count() as u16 + 1)
155 };
156
157 const THROBBER_WIDTH: u16 = 3;
159 let right_reserved = (if self.row_count.is_some() { 21 } else { 1 }) + THROBBER_WIDTH;
160 let mut available = area.width.saturating_sub(right_reserved);
161
162 let mut n_show = 0;
163 for pair in controls.iter() {
164 let need = pair_width(pair);
165 if available >= need {
166 available -= need;
167 n_show += 1;
168 } else {
169 break;
170 }
171 }
172
173 let mut constraints: Vec<Constraint> = controls
176 .iter()
177 .take(n_show)
178 .flat_map(|(key, action)| {
179 [
180 Constraint::Length(key.chars().count() as u16 + 1),
181 Constraint::Length(action.chars().count() as u16 + 1),
182 ]
183 })
184 .collect();
185
186 constraints.push(Constraint::Fill(1));
187 if self.row_count.is_some() {
188 constraints.push(Constraint::Length(20));
189 }
190 constraints.push(Constraint::Length(THROBBER_WIDTH));
191
192 let layout = Layout::new(Direction::Horizontal, constraints).split(area);
193
194 let (key_style, label_style, fill_style) = if no_bg {
195 (
196 Style::default().fg(self.key_color),
197 Style::default().fg(self.label_color),
198 Style::default(),
199 )
200 } else {
201 let base = Style::default().bg(self.bg_color);
202 (base.fg(self.key_color), base.fg(self.label_color), base)
203 };
204
205 for (i, (key, action)) in controls.iter().take(n_show).enumerate() {
206 let j = i * 2;
207 Paragraph::new(*key).style(key_style).render(layout[j], buf);
208 Paragraph::new(*action)
209 .style(label_style)
210 .render(layout[j + 1], buf);
211 }
212
213 let fill_idx = n_show * 2;
214 if let Some(count) = self.row_count {
215 let row_count_text = format!("Rows: {}", format_number_with_commas(count));
216 Paragraph::new(row_count_text)
217 .style(label_style)
218 .right_aligned()
219 .render(layout[fill_idx + 1], buf);
220 }
221
222 Paragraph::new("")
223 .style(fill_style)
224 .render(layout[fill_idx], buf);
225
226 const THROBBER_ASCII: [char; 4] = ['|', '/', '-', '\\'];
230 const THROBBER_BRAILLE_EIGHT: [char; 8] = ['⣷', '⣯', '⣟', '⡿', '⢿', '⣻', '⣽', '⣾'];
231 let throbber_idx = fill_idx + if self.row_count.is_some() { 2 } else { 1 };
232 let throbber_ch = if self.busy {
233 if self.use_unicode_throbber {
234 THROBBER_BRAILLE_EIGHT[self.throbber_frame as usize % 8].to_string()
235 } else {
236 THROBBER_ASCII[self.throbber_frame as usize % 4].to_string()
237 }
238 } else {
239 " ".to_string()
240 };
241 let throbber_style = if no_bg {
242 Style::default().fg(self.throbber_color)
243 } else {
244 Style::default().bg(self.bg_color).fg(self.throbber_color)
245 };
246 Paragraph::new(throbber_ch)
247 .style(throbber_style)
248 .centered()
249 .render(layout[throbber_idx], buf);
250 }
251}
252
253fn format_number_with_commas(n: usize) -> String {
254 let s = n.to_string();
255 let mut result = String::new();
256 let chars: Vec<char> = s.chars().rev().collect();
257
258 for (i, ch) in chars.iter().enumerate() {
259 if i > 0 && i % 3 == 0 {
260 result.push(',');
261 }
262 result.push(*ch);
263 }
264
265 result.chars().rev().collect()
266}