From 42036802b146085e6ced89d2a33a04338b103aaf Mon Sep 17 00:00:00 2001 From: flamestro Date: Thu, 28 May 2026 23:37:12 +0200 Subject: [PATCH] ensure correct key bindings are shown in legend --- src/config.rs | 61 +++++++++++++++++++++++++++++++++++++++++++++++++ src/render.rs | 27 +++++++++++++++++++++- src/terminal.rs | 6 +++-- 3 files changed, 91 insertions(+), 3 deletions(-) diff --git a/src/config.rs b/src/config.rs index 40ade21..3efcd05 100644 --- a/src/config.rs +++ b/src/config.rs @@ -54,6 +54,39 @@ impl KeyBinding { key.modifiers.contains(KeyModifiers::SHIFT) == self.modifiers.contains(KeyModifiers::SHIFT) } + + fn label(&self) -> String { + let mut parts = Vec::new(); + if self.modifiers.contains(KeyModifiers::CONTROL) { + parts.push("ctrl".to_string()); + } + if self.modifiers.contains(KeyModifiers::ALT) { + parts.push("alt".to_string()); + } + if self.modifiers.contains(KeyModifiers::SHIFT) + && !matches!(self.code, KeyCode::Char(ch) if ch.is_ascii_uppercase()) + { + parts.push("shift".to_string()); + } + + parts.push(match self.code { + KeyCode::Backspace => "backspace".to_string(), + KeyCode::Enter => "enter".to_string(), + KeyCode::Left => "left".to_string(), + KeyCode::Right => "right".to_string(), + KeyCode::Up => "up".to_string(), + KeyCode::Down => "down".to_string(), + KeyCode::Home => "home".to_string(), + KeyCode::End => "end".to_string(), + KeyCode::PageUp => "pageup".to_string(), + KeyCode::PageDown => "pagedown".to_string(), + KeyCode::Esc => "esc".to_string(), + KeyCode::Char(ch) => ch.to_string(), + _ => "?".to_string(), + }); + + parts.join("-") + } } #[derive(Clone, Debug)] @@ -127,6 +160,14 @@ impl KeyBindings { .is_some_and(|bindings| bindings.iter().any(|binding| binding.matches(key))) }) } + + pub(crate) fn primary_label_for_action(&self, action: Action) -> String { + self.bindings_by_action + .get(&action) + .and_then(|bindings| bindings.first()) + .map(KeyBinding::label) + .unwrap_or_else(|| "?".to_string()) + } } const ACTION_ORDER: &[Action] = &[ @@ -388,4 +429,24 @@ mod tests { Some(Action::PageDown) ); } + + #[test] + fn primary_label_uses_configured_binding() { + let mut bindings = KeyBindings::default(); + apply_config( + &mut bindings, + r#" + [keybindings] + page_down = "J" + previous_file = "alt-left" + "#, + ) + .expect("config should parse"); + + assert_eq!(bindings.primary_label_for_action(Action::PageDown), "J"); + assert_eq!( + bindings.primary_label_for_action(Action::PreviousFile), + "alt-left" + ); + } } diff --git a/src/render.rs b/src/render.rs index 537a317..c2957c7 100644 --- a/src/render.rs +++ b/src/render.rs @@ -12,6 +12,7 @@ use syntect::{ }; use crate::{ + config::{Action, KeyBindings}, model::{ DiffFileView, LineHighlightKind, PaneOffsets, PaneSide, ResolvedComparison, ThemeMode, }, @@ -139,6 +140,29 @@ fn syntax_for_language(language: &str) -> Option<&'static SyntaxReference> { .or_else(|| syntaxes.find_syntax_by_extension(language)) } +fn action_pair_label(keybindings: &KeyBindings, left: Action, right: Action) -> String { + format!( + "{}/{}", + keybindings.primary_label_for_action(left), + keybindings.primary_label_for_action(right) + ) +} + +fn key_legend(keybindings: &KeyBindings) -> String { + format!( + "{}: file {}: scroll {}: page {}: top/bottom {}: search {}: match {}: hunk {}: reviewed {}: quit", + action_pair_label(keybindings, Action::PreviousFile, Action::NextFile), + action_pair_label(keybindings, Action::ScrollDown, Action::ScrollUp), + action_pair_label(keybindings, Action::PageUp, Action::PageDown), + action_pair_label(keybindings, Action::Top, Action::Bottom), + keybindings.primary_label_for_action(Action::Search), + action_pair_label(keybindings, Action::NextMatch, Action::PreviousMatch), + action_pair_label(keybindings, Action::NextHunk, Action::PreviousHunk), + keybindings.primary_label_for_action(Action::ToggleReviewed), + keybindings.primary_label_for_action(Action::Quit), + ) +} + fn base_style(tint_background: Option) -> Style { let mut style = Style::default(); if let Some(color) = tint_background { @@ -336,6 +360,7 @@ pub(crate) fn get_pane_for_column(column: usize, layout: &FrameLayout) -> Option pub(crate) fn render_frame( files: &[DiffFileView], comparison: &ResolvedComparison, + keybindings: &KeyBindings, file_index: usize, scroll_offset: usize, pane_offsets: PaneOffsets, @@ -488,7 +513,7 @@ pub(crate) fn render_frame( layout.columns, ))); lines.push(Line::from(fit_line( - "h/l: file j/k: scroll ctrl-u/d: page g/G: top/bottom /: search n/N: match }/{: hunk r: reviewed q: quit", + &key_legend(keybindings), layout.columns, ))); lines.push(Line::from(fit_line( diff --git a/src/terminal.rs b/src/terminal.rs index 08d510b..042946c 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -26,12 +26,14 @@ fn draw_app( terminal: &mut Terminal, files: &[DiffFileView], comparison: &ResolvedComparison, + keybindings: &KeyBindings, app: &mut AppState, ) -> Result<()> { let size = terminal.size()?; let render_output = render_frame( files, comparison, + keybindings, app.file_index, app.scroll_offset, app.current_offsets(), @@ -65,7 +67,7 @@ fn run_event_loop( ) -> Result<()> { let initial_reviewed = review_store.reviewed_flags_for_files(files); let mut app = AppState::new(files.len(), initial_reviewed); - draw_app(terminal, files, comparison, &mut app)?; + draw_app(terminal, files, comparison, keybindings, &mut app)?; loop { match event::read().context("failed to read terminal event")? { @@ -96,7 +98,7 @@ fn run_event_loop( Event::FocusGained | Event::FocusLost | Event::Paste(_) => {} } - draw_app(terminal, files, comparison, &mut app)?; + draw_app(terminal, files, comparison, keybindings, &mut app)?; } Ok(())