use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use datui_cli::keys::{self, Chord, Code, Context, Key};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Line {
Key(&'static Key),
Note(&'static str, &'static str),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Block {
pub name: &'static str,
pub lines: Vec<Line>,
}
#[derive(Debug, Default)]
pub struct Help {
context: Option<Context>,
pub(crate) filter: String,
pub(crate) filtering: bool,
pub(crate) selected: usize,
pub(crate) scroll: usize,
typing: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HelpKey {
Stay,
Closed,
Press(KeyEvent),
}
impl Help {
pub fn is_open(&self) -> bool {
self.context.is_some()
}
pub fn context(&self) -> Option<Context> {
self.context
}
pub fn open(&mut self, context: Context, typing: bool) {
*self = Help {
context: Some(context),
typing,
..Help::default()
};
}
pub fn runnable(&self, key: &Key) -> Option<KeyEvent> {
let event = key_event(key.action()?);
let plain_char = matches!(event.code, KeyCode::Char(_))
&& !event
.modifiers
.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT);
(!(self.typing && plain_char)).then_some(event)
}
pub fn close(&mut self) {
*self = Help::default();
}
pub fn title(&self) -> String {
self.context
.map(|c| format!("{} Help", keys::screen(c).title))
.unwrap_or_default()
}
pub fn blocks(&self) -> Vec<Block> {
let Some(context) = self.context else {
return Vec::new();
};
let needle = self.filter.to_lowercase();
let keeps = |text: String| needle.is_empty() || text.to_lowercase().contains(&needle);
let mut blocks: Vec<Block> = Vec::new();
let mut push = |name: &'static str, lines: Vec<Line>| {
if !lines.is_empty() {
blocks.push(Block { name, lines });
}
};
let key_lines = |group: &'static keys::Group| -> Vec<Line> {
group
.keys
.iter()
.filter(|k| keeps(format!("{} {} {}", k.keys, k.label, k.line)))
.map(Line::Key)
.collect()
};
for group in keys::screen(context).groups {
push(group.name, key_lines(group));
}
if context == Context::Query {
let notes = keys::Q_SUMMARY
.iter()
.filter(|(example, meaning)| keeps(format!("{example} {meaning}")))
.map(|&(example, meaning)| Line::Note(example, meaning))
.collect();
push("q syntax", notes);
}
push(keys::GLOBAL.name, key_lines(&keys::GLOBAL));
blocks
}
pub fn shown_keys(&self) -> Vec<&'static Key> {
self.blocks()
.into_iter()
.flat_map(|b| b.lines)
.filter_map(|line| match line {
Line::Key(key) => Some(key),
Line::Note(..) => None,
})
.collect()
}
pub fn selected_key(&self) -> Option<&'static Key> {
let shown = self.shown_keys();
shown
.get(self.selected.min(shown.len().saturating_sub(1)))
.copied()
}
fn step(&mut self, by: isize) {
let count = self.shown_keys().len();
if count == 0 {
self.selected = 0;
return;
}
self.selected = self
.selected
.min(count - 1)
.saturating_add_signed(by)
.min(count - 1);
}
fn run(&mut self) -> HelpKey {
let Some(event) = self.selected_key().and_then(|k| self.runnable(k)) else {
return HelpKey::Stay;
};
self.close();
HelpKey::Press(event)
}
pub fn key(&mut self, event: &KeyEvent) -> HelpKey {
let ctrl = event.modifiers.contains(KeyModifiers::CONTROL);
match event.code {
KeyCode::Enter => return self.run(),
KeyCode::Up => self.step(-1),
KeyCode::Down => self.step(1),
KeyCode::PageUp => self.step(-10),
KeyCode::PageDown => self.step(10),
KeyCode::Char('p') if ctrl => self.step(-1),
KeyCode::Char('n') if ctrl => self.step(1),
KeyCode::Esc => {
if self.filtering || !self.filter.is_empty() {
self.filter.clear();
self.filtering = false;
self.selected = 0;
} else {
self.close();
return HelpKey::Closed;
}
}
KeyCode::F(1) => {
self.close();
return HelpKey::Closed;
}
_ if self.filtering => match event.code {
KeyCode::Backspace => {
self.filter.pop();
self.selected = 0;
}
KeyCode::Char('u') if ctrl => {
self.filter.clear();
self.selected = 0;
}
KeyCode::Char(c)
if !event
.modifiers
.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) =>
{
self.filter.push(c);
self.selected = 0;
}
_ => {}
},
KeyCode::Char('/') => self.filtering = true,
KeyCode::Char('?') => {
self.close();
return HelpKey::Closed;
}
KeyCode::Char('k') => self.step(-1),
KeyCode::Char('j') => self.step(1),
KeyCode::Home | KeyCode::Char('g') => self.selected = 0,
KeyCode::End | KeyCode::Char('G') => self.step(isize::MAX),
_ => {}
}
HelpKey::Stay
}
}
pub fn key_event(chord: Chord) -> KeyEvent {
let code = match chord.code {
Code::Char(c) => KeyCode::Char(c),
Code::Enter => KeyCode::Enter,
Code::Esc => KeyCode::Esc,
Code::Tab => KeyCode::Tab,
Code::BackTab => KeyCode::BackTab,
Code::Backspace => KeyCode::Backspace,
Code::Delete => KeyCode::Delete,
Code::Insert => KeyCode::Insert,
Code::Up => KeyCode::Up,
Code::Down => KeyCode::Down,
Code::Left => KeyCode::Left,
Code::Right => KeyCode::Right,
Code::PageUp => KeyCode::PageUp,
Code::PageDown => KeyCode::PageDown,
Code::Home => KeyCode::Home,
Code::End => KeyCode::End,
Code::F(n) => KeyCode::F(n),
};
let mut modifiers = KeyModifiers::NONE;
if chord.ctrl {
modifiers |= KeyModifiers::CONTROL;
}
if chord.alt {
modifiers |= KeyModifiers::ALT;
}
if chord.shift {
modifiers |= KeyModifiers::SHIFT;
}
KeyEvent::new(code, modifiers)
}
#[cfg(test)]
mod tests;