aboutsummaryrefslogtreecommitdiff
path: root/src/ui/keybind_help.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui/keybind_help.rs')
-rw-r--r--src/ui/keybind_help.rs426
1 files changed, 0 insertions, 426 deletions
diff --git a/src/ui/keybind_help.rs b/src/ui/keybind_help.rs
deleted file mode 100644
index 1f01c60c..00000000
--- a/src/ui/keybind_help.rs
+++ /dev/null
@@ -1,426 +0,0 @@
-use std::borrow::Cow;
-
-use ratatui::{
- layout::{Constraint, Layout, Rect},
- style::{Modifier, Style},
- text::{Line, Span},
- widgets::{Paragraph, Wrap},
- Frame,
-};
-
-use super::release_notes::release_notes_close_button_rect;
-use super::scrollbar::{release_notes_scrollbar_rect, render_scrollbar};
-use super::widgets::{
- modal_stack_areas, panel_contrast_fg, render_action_button, render_modal_header,
- render_modal_shell,
-};
-use crate::app::AppState;
-
-pub(super) type HelpEntry = (String, Cow<'static, str>);
-pub(super) type HelpGroup = (&'static str, Vec<HelpEntry>);
-
-fn help_entry(key: impl Into<String>, label: &'static str) -> HelpEntry {
- (key.into(), Cow::Borrowed(label))
-}
-
-fn keybind_label(bindings: &crate::config::ActionKeybinds) -> String {
- bindings.label().unwrap_or_else(|| "unset".to_string())
-}
-
-fn indexed_label(bindings: &[crate::config::IndexedKeybind]) -> String {
- if bindings.is_empty() {
- return "unset".to_string();
- }
-
- let mut parts = Vec::new();
- let mut index = 0;
- while index < bindings.len() {
- if let Some(prefix) = indexed_range_prefix(&bindings[index..]) {
- parts.push(format!("{prefix}1..9"));
- index += 9;
- } else {
- parts.push(bindings[index].label.clone());
- index += 1;
- }
- }
-
- parts.join(" / ")
-}
-
-fn indexed_range_prefix(bindings: &[crate::config::IndexedKeybind]) -> Option<&str> {
- let run = bindings.get(..9)?;
- let prefix = run[0].label.strip_suffix('1')?;
- for (offset, binding) in run.iter().enumerate() {
- let digit = char::from(b'1' + offset as u8);
- if binding.label.strip_suffix(digit) != Some(prefix) {
- return None;
- }
- }
- Some(prefix)
-}
-
-pub(super) fn keybind_help_groups(app: &AppState) -> Vec<HelpGroup> {
- let kb = &app.keybinds;
- let mut groups = Vec::new();
-
- groups.push((
- "global",
- vec![
- help_entry(
- crate::config::format_key_combo((app.prefix_code, app.prefix_mods)),
- "prefix mode",
- ),
- help_entry(keybind_label(&kb.help), "keybinds"),
- help_entry(keybind_label(&kb.settings), "settings"),
- help_entry(keybind_label(&kb.detach), "detach"),
- help_entry(keybind_label(&kb.reload_config), "reload config"),
- help_entry(
- keybind_label(&kb.open_notification_target),
- "open notification target",
- ),
- ],
- ));
-
- groups.push((
- "navigation",
- vec![
- help_entry("esc", "back"),
- help_entry(
- format!(
- "{} / {}",
- keybind_label(&kb.navigate.workspace_up),
- keybind_label(&kb.navigate.workspace_down)
- ),
- "workspace list",
- ),
- help_entry(
- format!(
- "{} / {} / {} / {} / left / right",
- keybind_label(&kb.navigate.pane_left),
- keybind_label(&kb.navigate.pane_down),
- keybind_label(&kb.navigate.pane_up),
- keybind_label(&kb.navigate.pane_right)
- ),
- "move focus",
- ),
- help_entry("tab / shift+tab", "cycle pane"),
- help_entry("enter", "open workspace"),
- help_entry("1..9", "switch workspace"),
- ],
- ));
-
- let workspace_tab = vec![
- help_entry(keybind_label(&kb.workspace_picker), "workspace navigation"),
- help_entry(keybind_label(&kb.goto), "session navigator"),
- help_entry(keybind_label(&kb.new_workspace), "new workspace"),
- help_entry(keybind_label(&kb.new_worktree), "new worktree"),
- help_entry(keybind_label(&kb.open_worktree), "open worktree"),
- help_entry(
- keybind_label(&kb.remove_worktree),
- "delete worktree checkout",
- ),
- help_entry(keybind_label(&kb.rename_workspace), "rename workspace"),
- help_entry(keybind_label(&kb.close_workspace), "close workspace"),
- help_entry(keybind_label(&kb.previous_workspace), "previous workspace"),
- help_entry(keybind_label(&kb.next_workspace), "next workspace"),
- help_entry(indexed_label(&kb.switch_workspace), "switch workspace 1-9"),
- help_entry(keybind_label(&kb.previous_agent), "previous agent"),
- help_entry(keybind_label(&kb.next_agent), "next agent"),
- help_entry(indexed_label(&kb.focus_agent), "focus agent 1-9"),
- help_entry(keybind_label(&kb.new_tab), "new tab"),
- help_entry(keybind_label(&kb.rename_tab), "rename tab"),
- help_entry(keybind_label(&kb.previous_tab), "previous tab"),
- help_entry(keybind_label(&kb.next_tab), "next tab"),
- help_entry(keybind_label(&kb.move_tab_previous), "move tab left"),
- help_entry(keybind_label(&kb.move_tab_next), "move tab right"),
- help_entry(indexed_label(&kb.switch_tab), "switch tab 1-9"),
- help_entry(keybind_label(&kb.close_tab), "close tab"),
- ];
- groups.push(("workspaces / tabs", workspace_tab));
-
- let panes = vec![
- help_entry(keybind_label(&kb.split_vertical), "split vertical"),
- help_entry(keybind_label(&kb.split_horizontal), "split horizontal"),
- help_entry(keybind_label(&kb.close_pane), "close pane"),
- help_entry(keybind_label(&kb.rename_pane), "rename pane"),
- help_entry(keybind_label(&kb.edit_scrollback), "edit scrollback"),
- help_entry(keybind_label(&kb.copy_mode), "copy mode"),
- help_entry(keybind_label(&kb.zoom), "zoom pane"),
- help_entry(keybind_label(&kb.resize_mode), "resize mode"),
- help_entry(keybind_label(&kb.resize_pane_left), "resize pane left"),
- help_entry(keybind_label(&kb.resize_pane_down), "resize pane down"),
- help_entry(keybind_label(&kb.resize_pane_up), "resize pane up"),
- help_entry(keybind_label(&kb.resize_pane_right), "resize pane right"),
- help_entry(keybind_label(&kb.toggle_sidebar), "toggle sidebar"),
- help_entry(keybind_label(&kb.focus_pane_left), "focus pane left"),
- help_entry(keybind_label(&kb.focus_pane_down), "focus pane down"),
- help_entry(keybind_label(&kb.focus_pane_up), "focus pane up"),
- help_entry(keybind_label(&kb.focus_pane_right), "focus pane right"),
- help_entry(keybind_label(&kb.cycle_pane_next), "cycle pane next"),
- help_entry(
- keybind_label(&kb.cycle_pane_previous),
- "cycle pane previous",
- ),
- help_entry(keybind_label(&kb.last_pane), "last pane"),
- ];
- groups.push(("panes", panes));
-
- if !kb.custom_commands.is_empty() {
- groups.push((
- "custom",
- kb.custom_commands
- .iter()
- .map(|binding| {
- (
- binding.label.clone(),
- binding
- .description
- .clone()
- .map(Cow::Owned)
- .unwrap_or(Cow::Borrowed("custom command")),
- )
- })
- .collect(),
- ));
- }
-
- groups
-}
-
-fn filter_keybind_help_groups(groups: Vec<HelpGroup>, query: &str) -> Vec<HelpGroup> {
- if query.is_empty() {
- return groups;
- }
-
- let query = query.to_lowercase();
- groups
- .into_iter()
- .filter_map(|(group, entries)| {
- let entries = entries
- .into_iter()
- .filter(|(key, label)| {
- key.to_lowercase().contains(&query) || label.to_lowercase().contains(&query)
- })
- .collect::<Vec<_>>();
- (!entries.is_empty()).then_some((group, entries))
- })
- .collect()
-}
-
-pub(crate) fn keybind_help_lines(app: &AppState) -> Vec<(usize, Line<'static>)> {
- let heading_style = Style::default()
- .fg(app.palette.accent)
- .add_modifier(Modifier::BOLD);
- let key_style = Style::default()
- .fg(app.palette.mauve)
- .add_modifier(Modifier::BOLD);
- let label_style = Style::default().fg(app.palette.text);
-
- let groups = filter_keybind_help_groups(keybind_help_groups(app), &app.keybind_help.query);
- let key_width = groups
- .iter()
- .flat_map(|(_, entries)| entries.iter().map(|(key, _)| key.chars().count()))
- .max()
- .unwrap_or(8);
-
- let mut lines = Vec::new();
-
- if groups.is_empty() {
- let message = " no matching keybinds";
- return vec![(
- message.chars().count(),
- Line::from(Span::styled(
- message,
- Style::default().fg(app.palette.overlay1),
- )),
- )];
- }
-
- for (group, entries) in groups {
- lines.push((
- group.len() + 1,
- Line::from(vec![Span::styled(format!(" {group}"), heading_style)]),
- ));
- for (key, label) in entries {
- let padded_key = format!(" {:<width$} ", key, width = key_width);
- let width = padded_key.chars().count() + label.chars().count();
- lines.push((
- width,
- Line::from(vec![
- Span::styled(padded_key, key_style),
- Span::styled(label.into_owned(), label_style),
- ]),
- ));
- }
- lines.push((0, Line::raw("")));
- }
-
- lines
-}
-
-pub(super) fn render_keybind_help_overlay(app: &AppState, frame: &mut Frame) {
- super::dim_background(frame, frame.area());
-
- let Some(inner) = render_modal_shell(frame, frame.area(), 76, 22, &app.palette) else {
- return;
- };
- if inner.height < 6 || inner.width < 20 {
- return;
- }
-
- let stack = modal_stack_areas(inner, 2, 1, 0, 1);
- let header_rows =
- Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas::<2>(stack.header);
-
- render_modal_header(frame, header_rows[0], "keybinds", &app.palette);
- render_action_button(
- frame,
- release_notes_close_button_rect(header_rows[0]),
- Some("esc"),
- if app.keybind_help.search_focused {
- "back"
- } else {
- "close"
- },
- Style::default()
- .fg(panel_contrast_fg(&app.palette))
- .bg(app.palette.accent)
- .add_modifier(Modifier::BOLD),
- );
- let search_line = if app.keybind_help.search_focused {
- Line::from(vec![
- Span::styled(
- " / ",
- Style::default()
- .fg(app.palette.accent)
- .add_modifier(Modifier::BOLD),
- ),
- Span::styled(
- app.keybind_help.query.as_str(),
- Style::default()
- .fg(app.palette.text)
- .add_modifier(Modifier::BOLD),
- ),
- ])
- } else {
- Line::from(Span::styled(
- " press / to filter by command or shortcut",
- Style::default().fg(app.palette.overlay0),
- ))
- };
- frame.render_widget(Paragraph::new(search_line), header_rows[1]);
-
- let body_area = stack.content;
- let metrics = crate::pane::ScrollMetrics {
- offset_from_bottom: app
- .keybind_help_max_scroll()
- .saturating_sub(app.keybind_help.scroll) as usize,
- max_offset_from_bottom: app.keybind_help_max_scroll() as usize,
- viewport_rows: body_area.height.max(1) as usize,
- };
- let track = release_notes_scrollbar_rect(body_area, metrics);
- let text_area = track
- .map(|_| {
- Rect::new(
- body_area.x,
- body_area.y,
- body_area.width.saturating_sub(1),
- body_area.height,
- )
- })
- .unwrap_or(body_area);
-
- let body = Paragraph::new(
- keybind_help_lines(app)
- .into_iter()
- .map(|(_, line)| line)
- .collect::<Vec<_>>(),
- )
- .wrap(Wrap { trim: false })
- .scroll((app.keybind_help.scroll, 0));
- frame.render_widget(body, text_area);
- if let Some(track) = track {
- render_scrollbar(
- frame,
- metrics,
- track,
- app.palette.overlay0,
- app.palette.overlay1,
- "▐",
- );
- }
-
- let footer = if app.keybind_help.search_focused {
- Line::from(vec![
- Span::styled(" filter ", Style::default().fg(app.palette.overlay0)),
- Span::styled("type/backspace", Style::default().fg(app.palette.text)),
- Span::styled(" · ", Style::default().fg(app.palette.overlay0)),
- Span::styled("clear ", Style::default().fg(app.palette.overlay0)),
- Span::styled("ctrl+u", Style::default().fg(app.palette.text)),
- Span::styled(" · ", Style::default().fg(app.palette.overlay0)),
- Span::styled("scroll ", Style::default().fg(app.palette.overlay0)),
- Span::styled("↑↓/pgup/pgdn", Style::default().fg(app.palette.text)),
- Span::styled(" · ", Style::default().fg(app.palette.overlay0)),
- Span::styled("back ", Style::default().fg(app.palette.overlay0)),
- Span::styled("esc", Style::default().fg(app.palette.text)),
- ])
- } else {
- Line::from(vec![
- Span::styled(" search ", Style::default().fg(app.palette.overlay0)),
- Span::styled("/", Style::default().fg(app.palette.text)),
- Span::styled(" · ", Style::default().fg(app.palette.overlay0)),
- Span::styled("scroll ", Style::default().fg(app.palette.overlay0)),
- Span::styled("j/k/↑↓/pgup/pgdn", Style::default().fg(app.palette.text)),
- Span::styled(" · ", Style::default().fg(app.palette.overlay0)),
- Span::styled("close ", Style::default().fg(app.palette.overlay0)),
- Span::styled("esc/enter", Style::default().fg(app.palette.text)),
- ])
- };
- frame.render_widget(Paragraph::new(footer), stack.footer.unwrap_or_default());
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- fn groups() -> Vec<HelpGroup> {
- vec![
- (
- "workspaces / tabs",
- vec![
- help_entry("w", "workspace navigation"),
- help_entry("c", "new tab"),
- ],
- ),
- (
- "panes",
- vec![
- help_entry("v", "split vertical"),
- help_entry("x", "close pane"),
- ],
- ),
- ]
- }
-
- #[test]
- fn keybind_help_filter_matches_labels_case_insensitively() {
- let filtered = filter_keybind_help_groups(groups(), "WoRk");
-
- assert_eq!(filtered.len(), 1);
- assert_eq!(filtered[0].0, "workspaces / tabs");
- assert_eq!(filtered[0].1.len(), 1);
- assert_eq!(filtered[0].1[0].1, "workspace navigation");
- }
-
- #[test]
- fn keybind_help_filter_matches_shortcuts_without_matching_group_headings() {
- let filtered = filter_keybind_help_groups(groups(), "x");
-
- assert_eq!(filtered.len(), 1);
- assert_eq!(filtered[0].0, "panes");
- assert_eq!(filtered[0].1.len(), 1);
- assert_eq!(filtered[0].1[0].1, "close pane");
-
- assert!(filter_keybind_help_groups(groups(), "panes").is_empty());
- }
-}