Files
skyshift/src/ui/popups/help.rs
T
skkeyeandClaude Opus 5.5 255dcd1392 Add popups: segment editor, copy menu, export, help and command line
- 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]>
2026-09-24 02:59:22 -04:00

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
}
}