1use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
12use datui_cli::keys::{self, Chord, Code, Context, Key};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Line {
17 Key(&'static Key),
19 Note(&'static str, &'static str),
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct Block {
26 pub name: &'static str,
27 pub lines: Vec<Line>,
28}
29
30#[derive(Debug, Default)]
32pub struct Help {
33 context: Option<Context>,
34 pub(crate) filter: String,
36 pub(crate) filtering: bool,
38 pub(crate) selected: usize,
40 pub(crate) scroll: usize,
42 typing: bool,
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum HelpKey {
50 Stay,
52 Closed,
54 Press(KeyEvent),
56}
57
58impl Help {
59 pub fn is_open(&self) -> bool {
60 self.context.is_some()
61 }
62
63 pub fn context(&self) -> Option<Context> {
65 self.context
66 }
67
68 pub fn open(&mut self, context: Context, typing: bool) {
70 *self = Help {
71 context: Some(context),
72 typing,
73 ..Help::default()
74 };
75 }
76
77 pub fn runnable(&self, key: &Key) -> Option<KeyEvent> {
80 let event = key_event(key.action()?);
81 let plain_char = matches!(event.code, KeyCode::Char(_))
82 && !event
83 .modifiers
84 .intersects(KeyModifiers::CONTROL | KeyModifiers::ALT);
85 (!(self.typing && plain_char)).then_some(event)
86 }
87
88 pub fn close(&mut self) {
89 *self = Help::default();
90 }
91
92 pub fn title(&self) -> String {
94 self.context
95 .map(|c| format!("{} Help", keys::screen(c).title))
96 .unwrap_or_default()
97 }
98
99 pub fn blocks(&self) -> Vec<Block> {
102 let Some(context) = self.context else {
103 return Vec::new();
104 };
105 let needle = self.filter.to_lowercase();
106 let keeps = |text: String| needle.is_empty() || text.to_lowercase().contains(&needle);
107 let mut blocks: Vec<Block> = Vec::new();
108 let mut push = |name: &'static str, lines: Vec<Line>| {
109 if !lines.is_empty() {
110 blocks.push(Block { name, lines });
111 }
112 };
113 let key_lines = |group: &'static keys::Group| -> Vec<Line> {
114 group
115 .keys
116 .iter()
117 .filter(|k| keeps(format!("{} {} {}", k.keys, k.label, k.line)))
118 .map(Line::Key)
119 .collect()
120 };
121 for group in keys::screen(context).groups {
122 push(group.name, key_lines(group));
123 }
124 if context == Context::Query {
125 let notes = keys::Q_SUMMARY
126 .iter()
127 .filter(|(example, meaning)| keeps(format!("{example} {meaning}")))
128 .map(|&(example, meaning)| Line::Note(example, meaning))
129 .collect();
130 push("q syntax", notes);
131 }
132 push(keys::GLOBAL.name, key_lines(&keys::GLOBAL));
133 blocks
134 }
135
136 pub fn shown_keys(&self) -> Vec<&'static Key> {
138 self.blocks()
139 .into_iter()
140 .flat_map(|b| b.lines)
141 .filter_map(|line| match line {
142 Line::Key(key) => Some(key),
143 Line::Note(..) => None,
144 })
145 .collect()
146 }
147
148 pub fn selected_key(&self) -> Option<&'static Key> {
150 let shown = self.shown_keys();
151 shown
152 .get(self.selected.min(shown.len().saturating_sub(1)))
153 .copied()
154 }
155
156 fn step(&mut self, by: isize) {
157 let count = self.shown_keys().len();
158 if count == 0 {
159 self.selected = 0;
160 return;
161 }
162 self.selected = self
163 .selected
164 .min(count - 1)
165 .saturating_add_signed(by)
166 .min(count - 1);
167 }
168
169 fn run(&mut self) -> HelpKey {
171 let Some(event) = self.selected_key().and_then(|k| self.runnable(k)) else {
172 return HelpKey::Stay;
173 };
174 self.close();
175 HelpKey::Press(event)
176 }
177
178 pub fn key(&mut self, event: &KeyEvent) -> HelpKey {
180 let ctrl = event.modifiers.contains(KeyModifiers::CONTROL);
181 match event.code {
182 KeyCode::Enter => return self.run(),
183 KeyCode::Up => self.step(-1),
184 KeyCode::Down => self.step(1),
185 KeyCode::PageUp => self.step(-10),
186 KeyCode::PageDown => self.step(10),
187 KeyCode::Char('p') if ctrl => self.step(-1),
188 KeyCode::Char('n') if ctrl => self.step(1),
189 KeyCode::Esc => {
190 if self.filtering || !self.filter.is_empty() {
191 self.filter.clear();
192 self.filtering = false;
193 self.selected = 0;
194 } else {
195 self.close();
196 return HelpKey::Closed;
197 }
198 }
199 KeyCode::F(1) => {
200 self.close();
201 return HelpKey::Closed;
202 }
203 _ if self.filtering => match event.code {
204 KeyCode::Backspace => {
205 self.filter.pop();
206 self.selected = 0;
207 }
208 KeyCode::Char('u') if ctrl => {
209 self.filter.clear();
210 self.selected = 0;
211 }
212 KeyCode::Char(c)
213 if !event
214 .modifiers
215 .intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) =>
216 {
217 self.filter.push(c);
218 self.selected = 0;
219 }
220 _ => {}
221 },
222 KeyCode::Char('/') => self.filtering = true,
223 KeyCode::Char('?') => {
224 self.close();
225 return HelpKey::Closed;
226 }
227 KeyCode::Char('k') => self.step(-1),
228 KeyCode::Char('j') => self.step(1),
229 KeyCode::Home | KeyCode::Char('g') => self.selected = 0,
230 KeyCode::End | KeyCode::Char('G') => self.step(isize::MAX),
231 _ => {}
232 }
233 HelpKey::Stay
234 }
235}
236
237pub fn key_event(chord: Chord) -> KeyEvent {
239 let code = match chord.code {
240 Code::Char(c) => KeyCode::Char(c),
241 Code::Enter => KeyCode::Enter,
242 Code::Esc => KeyCode::Esc,
243 Code::Tab => KeyCode::Tab,
244 Code::BackTab => KeyCode::BackTab,
245 Code::Backspace => KeyCode::Backspace,
246 Code::Delete => KeyCode::Delete,
247 Code::Insert => KeyCode::Insert,
248 Code::Up => KeyCode::Up,
249 Code::Down => KeyCode::Down,
250 Code::Left => KeyCode::Left,
251 Code::Right => KeyCode::Right,
252 Code::PageUp => KeyCode::PageUp,
253 Code::PageDown => KeyCode::PageDown,
254 Code::Home => KeyCode::Home,
255 Code::End => KeyCode::End,
256 Code::F(n) => KeyCode::F(n),
257 };
258 let mut modifiers = KeyModifiers::NONE;
259 if chord.ctrl {
260 modifiers |= KeyModifiers::CONTROL;
261 }
262 if chord.alt {
263 modifiers |= KeyModifiers::ALT;
264 }
265 if chord.shift {
266 modifiers |= KeyModifiers::SHIFT;
267 }
268 KeyEvent::new(code, modifiers)
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 impl Help {
276 fn open_for_test(&mut self, context: Context) {
277 self.open(context, false);
278 }
279 }
280
281 #[test]
284 fn a_text_field_keeps_its_characters() {
285 let mut help = Help::default();
286 help.open(Context::Find, true);
287 press(&mut help, KeyCode::Char('/'));
288 type_text(&mut help, "regex on");
289 assert_eq!(
290 press(&mut help, KeyCode::Enter),
291 HelpKey::Press(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL))
292 );
293 help.open(Context::Export, true);
294 let space = keys::screen(Context::Export).groups[0]
295 .keys
296 .iter()
297 .find(|k| k.keys == "Space")
298 .unwrap();
299 assert_eq!(help.runnable(space), None);
300 help.open(Context::Export, false);
301 assert!(help.runnable(space).is_some());
302 }
303
304 fn press(help: &mut Help, code: KeyCode) -> HelpKey {
305 help.key(&KeyEvent::new(code, KeyModifiers::NONE))
306 }
307
308 fn type_text(help: &mut Help, text: &str) {
309 for c in text.chars() {
310 press(help, KeyCode::Char(c));
311 }
312 }
313
314 #[test]
315 fn every_screen_ends_with_the_keys_of_every_screen() {
316 for screen in keys::SCREENS {
317 let mut help = Help::default();
318 help.open_for_test(screen.context);
319 let blocks = help.blocks();
320 assert_eq!(blocks.last().map(|b| b.name), Some(keys::GLOBAL.name));
321 assert!(blocks.len() > 1, "{}", screen.title);
322 }
323 }
324
325 #[test]
326 fn the_query_screen_carries_the_q_summary() {
327 let mut help = Help::default();
328 help.open_for_test(Context::Query);
329 assert!(help.blocks().iter().any(|b| b.name == "q syntax"));
330 help.open_for_test(Context::Table);
331 assert!(!help.blocks().iter().any(|b| b.name == "q syntax"));
332 }
333
334 #[test]
335 fn slash_narrows_by_key_and_description() {
336 let mut help = Help::default();
337 help.open_for_test(Context::Table);
338 let all = help.shown_keys().len();
339 press(&mut help, KeyCode::Char('/'));
340 type_text(&mut help, "value counts");
341 let shown = help.shown_keys();
342 assert_eq!(shown.len(), 1, "{shown:?}");
343 assert_eq!(shown[0].keys, "F");
344 press(&mut help, KeyCode::Backspace);
346 type_text(&mut help, "s");
347 assert_eq!(help.filter, "value counts");
348 assert_eq!(press(&mut help, KeyCode::Esc), HelpKey::Stay);
350 assert_eq!(help.shown_keys().len(), all);
351 assert!(help.is_open());
352 assert_eq!(press(&mut help, KeyCode::Esc), HelpKey::Closed);
353 assert!(!help.is_open());
354 }
355
356 #[test]
357 fn a_filter_that_matches_nothing_shows_nothing() {
358 let mut help = Help::default();
359 help.open_for_test(Context::Table);
360 press(&mut help, KeyCode::Char('/'));
361 type_text(&mut help, "zzzzzz");
362 assert!(help.blocks().is_empty());
363 assert_eq!(press(&mut help, KeyCode::Enter), HelpKey::Stay);
364 assert!(help.is_open());
365 }
366
367 #[test]
368 fn enter_closes_and_presses_the_selected_key() {
369 let mut help = Help::default();
370 help.open_for_test(Context::Table);
371 press(&mut help, KeyCode::Char('/'));
372 type_text(&mut help, "value counts");
373 let pressed = press(&mut help, KeyCode::Enter);
374 assert_eq!(
375 pressed,
376 HelpKey::Press(KeyEvent::new(KeyCode::Char('F'), KeyModifiers::SHIFT))
377 );
378 assert!(!help.is_open());
379 }
380
381 #[test]
382 fn arrows_walk_the_keys_and_stop_at_the_ends() {
383 let mut help = Help::default();
384 help.open_for_test(Context::Query);
385 let count = help.shown_keys().len();
386 press(&mut help, KeyCode::Up);
387 assert_eq!(help.selected, 0);
388 press(&mut help, KeyCode::Char('j'));
389 assert_eq!(help.selected, 1);
390 press(&mut help, KeyCode::End);
391 assert_eq!(help.selected, count - 1);
392 press(&mut help, KeyCode::Down);
393 assert_eq!(help.selected, count - 1);
394 press(&mut help, KeyCode::Home);
395 assert_eq!(help.selected, 0);
396 }
397
398 #[test]
400 fn enter_on_a_line_with_no_key_stays() {
401 let mut help = Help::default();
402 help.open_for_test(Context::Query);
403 assert_eq!(help.selected_key().map(|k| k.keys), Some("(digits)"));
404 assert_eq!(press(&mut help, KeyCode::Enter), HelpKey::Stay);
405 assert!(help.is_open());
406 }
407}