//! Every key, generated from the key tables (so it's never out of date). use ratatui::Frame; use ratatui::layout::Rect; use ratatui::style::Style; use ratatui::text::{Line, Span}; use ratatui::widgets::Paragraph; use super::command_line::COMMANDS; use super::window; use crate::action::Action; use crate::ui::keymap::{self, Binding, Command}; use crate::ui::{Component, Context}; const TYPING: &[(&str, &str)] = &[ ("", "confirm"), ("", "back to normal mode (Esc again closes)"), (" ", "next / previous field"), ("", "delete the word before the cursor"), ("", "delete everything before the cursor"), (" ", "move the cursor"), ]; pub struct Help { sections: Vec<(String, Vec<(String, String)>)>, scroll: usize, /// Lines that fit on screen (known after drawing), for paging. visible: usize, } impl Help { /// Help for a screen: its own keys first, then everything else. pub fn new(screen: &str, screen_tables: &[&[Binding]]) -> Help { let pairs = |rows: &[(&str, &str)]| { rows.iter().map(|(keys, help)| (keys.to_string(), help.to_string())).collect() }; Help { sections: vec![ (format!("{screen} screen"), merged(screen_tables)), ("Everywhere".into(), merged(&[keymap::GLOBAL])), ("In popups (normal mode)".into(), merged(&[keymap::POPUP])), ("Typing (insert mode)".into(), pairs(TYPING)), ("Command line".into(), pairs(COMMANDS)), ], scroll: 0, visible: 10, } } fn lines(&self, ctx: &Context) -> Vec> { let mut lines = Vec::new(); for (title, rows) in &self.sections { if rows.is_empty() { continue; } lines.push(Line::styled(format!(" {title}"), ctx.theme.title())); for (keys, help) in rows { lines.push(Line::from(vec![ Span::styled(format!(" {keys:<28}"), ctx.theme.accent()), Span::styled(help.clone(), Style::new()), ])); } lines.push(Line::default()); } lines } } /// The bindings of some tables as (keys, help). Keys that do the same /// thing are merged on one line: "j ". fn merged(tables: &[&[Binding]]) -> Vec<(String, String)> { let mut rows: Vec<(String, String)> = Vec::new(); for binding in tables.iter().flat_map(|table| table.iter()) { match rows.iter_mut().find(|(_, help)| help == binding.help) { Some((keys, _)) => { keys.push(' '); keys.push_str(binding.keys); } None => rows.push((binding.keys.to_string(), binding.help.to_string())), } } rows } impl Component for Help { fn keys(&self) -> &'static [&'static [Binding]] { &[keymap::POPUP] } fn render(&mut self, frame: &mut Frame, area: Rect, ctx: &Context) { let width = area.width.saturating_sub(8).min(100); let height = area.height.saturating_sub(4); let inner = window(frame, area, width, height, "Keys", "j/k scroll ยท q close", ctx.theme); let lines = self.lines(ctx); self.visible = usize::from(inner.height).max(1); self.scroll = self.scroll.min(lines.len().saturating_sub(self.visible)); frame.render_widget(Paragraph::new(lines).scroll((self.scroll as u16, 0)), inner); } fn command(&mut self, command: Command, count: Option, _ctx: &Context) -> Option { let steps = count.unwrap_or(1); match command { Command::Down => self.scroll += steps, Command::Up => self.scroll = self.scroll.saturating_sub(steps), Command::HalfPageDown => self.scroll += self.visible / 2, Command::HalfPageUp => self.scroll = self.scroll.saturating_sub(self.visible / 2), Command::Top => self.scroll = 0, Command::Bottom => self.scroll = usize::MAX / 2, Command::Close | Command::Confirm => return Some(Action::ClosePopup), _ => {} } None } }