Skip to main content

datui_lib/app/
help.rs

1//! The help overlay: one screen's keys from the key registry
2//! ([`datui_cli::keys`]), grouped by task, the keys of every screen last.
3//!
4//! `/` narrows the lines to those whose key, label or description holds what is
5//! typed. Enter closes the help and presses the key on the selected line, as the
6//! next key the app takes: [`Help::key`] answers [`HelpKey::Press`], and the App
7//! hands that key back to the event loop as its follow-up `AppEvent::Key`, so it
8//! takes the path a typed key takes (held while busy, replayed in order) rather
9//! than a parallel set of actions.
10
11use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
12use datui_cli::keys::{self, Chord, Code, Context, Key};
13
14/// What a line of the help holds.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Line {
17    /// A key, which Enter can press.
18    Key(&'static Key),
19    /// An example and what it means: the q summary on the query screen.
20    Note(&'static str, &'static str),
21}
22
23/// One group's lines, under its name.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct Block {
26    pub name: &'static str,
27    pub lines: Vec<Line>,
28}
29
30/// The overlay's state: open over a context, or closed.
31#[derive(Debug, Default)]
32pub struct Help {
33    context: Option<Context>,
34    /// What `/` narrowed the lines to.
35    pub(crate) filter: String,
36    /// Whether typed characters go to the filter.
37    pub(crate) filtering: bool,
38    /// The selected key, among the keys shown.
39    pub(crate) selected: usize,
40    /// The first line drawn; the render keeps the selection in view and writes it back.
41    pub(crate) scroll: usize,
42    /// Whether a text field under the help takes typed characters: a plain character
43    /// pressed there would type, so those lines do not run.
44    typing: bool,
45}
46
47/// What a key typed at the help did.
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum HelpKey {
50    /// Handled inside the help.
51    Stay,
52    /// The help closed.
53    Closed,
54    /// The help closed; press this key where it was opened.
55    Press(KeyEvent),
56}
57
58impl Help {
59    pub fn is_open(&self) -> bool {
60        self.context.is_some()
61    }
62
63    /// The screen whose keys are shown.
64    pub fn context(&self) -> Option<Context> {
65        self.context
66    }
67
68    /// Open on `context`'s keys; `typing` when a text field there takes characters.
69    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    /// The key Enter presses for `key` here: none for a plain character while a text
78    /// field would type it.
79    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    /// The overlay's title: `Table Help`.
93    pub fn title(&self) -> String {
94        self.context
95            .map(|c| format!("{} Help", keys::screen(c).title))
96            .unwrap_or_default()
97    }
98
99    /// The blocks shown: the screen's groups, the q summary on the query screen, then
100    /// the keys of every screen; each narrowed by the filter, empty ones left out.
101    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    /// The keys shown, in order: what ↑↓ walk.
137    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    /// The selected key, if any is shown.
149    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    /// Enter: close, and press the selected line's key if it has one.
170    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    /// A key typed while the help is open.
179    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
237/// The key event a chord stands for, as a terminal reports it.
238pub 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;