- segment_editor: type start/end times; the exact moments they mean are shown live, and invalid edits keep the popup open. - time_prompt: "I started / paused / ended the day at...". - copy_menu: every copy preset with a preview of what will be pasted. - export_dialog, still_working (for sessions left running very long), help (generated from the key tables) and the `:` command line. Co-Authored-By: Claude Opus 5.5 <[email protected]>
115 lines
4.1 KiB
Rust
115 lines
4.1 KiB
Rust
//! 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)] = &[
|
|
("<CR>", "confirm"),
|
|
("<Esc>", "back to normal mode (Esc again closes)"),
|
|
("<Tab> <S-Tab>", "next / previous field"),
|
|
("<C-w>", "delete the word before the cursor"),
|
|
("<C-u>", "delete everything before the cursor"),
|
|
("<Left> <Right> <Home> <End>", "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<Line<'static>> {
|
|
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 <Down>".
|
|
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<usize>, _ctx: &Context) -> Option<Action> {
|
|
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
|
|
}
|
|
}
|