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]>
This commit is contained in:
1 parent
7c8a02275e
commit
255dcd1392
9 files changed
+1231
No files matched your search
@@ -0,0 +1,114 @@
|
||||
//! 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
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user