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