Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions src/config/keybindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,34 @@ pub fn parse_key(s: &str) -> Option<KeyEvent> {
None
}

/// Check if a `KeyEvent` matches the configured keybinding string.
/// Character keys are matched case-insensitively while preserving modifier equality.
/// Shifted punctuation arriving with SHIFT modifier is normalized when the expected binding has no modifiers.
pub fn matches_key(mut key: KeyEvent, binding: &str) -> bool {
if let Some(expected) = parse_key(binding) {
if expected.modifiers.is_empty()
&& matches!(expected.code, KeyCode::Char(c) if c.is_ascii_punctuation())
{
key.modifiers.remove(KeyModifiers::SHIFT);
}
let code_match = match (key.code, expected.code) {
(KeyCode::Char(c1), KeyCode::Char(c2)) => {
c1.to_ascii_lowercase() == c2.to_ascii_lowercase()
}
(code1, code2) => code1 == code2,
};
code_match && key.modifiers == expected.modifiers
} else {
false
}
}

impl KeybindingConfig {
pub fn matches_key(&self, key: KeyEvent, binding: &str) -> bool {
matches_key(key, binding)
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -542,4 +570,23 @@ universal:
assert_eq!(parsed.universal.tree_prev_sibling, "h");
assert_eq!(parsed.universal.tree_next_sibling, "l");
}

#[test]
fn test_matches_key_case_insensitive() {
let key_upper = KeyEvent::new(KeyCode::Char('L'), KeyModifiers::CONTROL);
let key_lower = KeyEvent::new(KeyCode::Char('l'), KeyModifiers::CONTROL);
assert!(matches_key(key_upper, "<c-l>"));
assert!(matches_key(key_lower, "<c-l>"));
assert!(matches_key(key_upper, "<c-L>"));
assert!(matches_key(key_lower, "<c-L>"));

let key_shift_d = KeyEvent::new(KeyCode::Char('D'), KeyModifiers::SHIFT);
let key_lower_d = KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE);
assert!(matches_key(key_shift_d, "D"));
assert!(matches_key(key_lower_d, "d"));
assert!(!matches_key(key_lower_d, "D"));

// Different modifiers must NOT match
assert!(!matches_key(key_lower, "l"));
}
}
2 changes: 1 addition & 1 deletion src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use std::path::PathBuf;
use anyhow::Result;

pub use app_state::AppState;
pub use keybindings::KeybindingConfig;
pub use keybindings::{KeybindingConfig, matches_key};
pub use theme::{COLOR_THEMES, ColorTheme, Theme, ThemeAppearance};
pub use user_config::UserConfig;

Expand Down
3 changes: 2 additions & 1 deletion src/gui/controller/branch_commits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ use anyhow::Result;
use crossterm::event::{KeyCode, KeyEvent};

use crate::config::KeybindingConfig;
use crate::config::matches_key;
use crate::gui::Gui;
use crate::gui::context::ContextId;

pub fn handle_key(gui: &mut Gui, key: KeyEvent, keybindings: &KeybindingConfig) -> Result<()> {
if super::commits::matches_key(key, &keybindings.commits.open_log_menu) {
if matches_key(key, &keybindings.commits.open_log_menu) {
return super::commits::show_filtering_menu(gui);
}

Expand Down
12 changes: 2 additions & 10 deletions src/gui/controller/branches.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ use anyhow::Result;
use crossterm::event::{KeyCode, KeyEvent};

use crate::config::KeybindingConfig;
use crate::config::keybindings::parse_key;
use crate::config::matches_key;
use crate::gui::Gui;
use crate::gui::popup::{MenuItem, MessageKind, PopupState, make_textarea};
use crate::os::platform::Platform;

pub fn handle_key(gui: &mut Gui, key: KeyEvent, keybindings: &KeybindingConfig) -> Result<()> {
if super::commits::matches_key(key, &keybindings.commits.open_log_menu) {
if matches_key(key, &keybindings.commits.open_log_menu) {
return super::commits::show_filtering_menu(gui);
}

Expand Down Expand Up @@ -768,11 +768,3 @@ fn open_in_browser_menu(gui: &mut Gui) -> Result<()> {
}
Ok(())
}

fn matches_key(key: KeyEvent, binding: &str) -> bool {
if let Some(expected) = parse_key(binding) {
key.code == expected.code && key.modifiers == expected.modifiers
} else {
false
}
}
16 changes: 3 additions & 13 deletions src/gui/controller/commit_files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use anyhow::Result;
use crossterm::event::{KeyCode, KeyEvent};

use crate::config::KeybindingConfig;
use crate::config::keybindings::parse_key;
use crate::config::matches_key;
use crate::gui::Gui;
use crate::gui::context::ContextId;
use crate::gui::popup::{MenuItem, PopupState};
Expand All @@ -13,7 +13,7 @@ pub fn handle_key(gui: &mut Gui, key: KeyEvent, keybindings: &KeybindingConfig)
if super::diff_grep::is_diff_grep_key(key) {
return super::diff_grep::open_diff_grep_picker(gui);
}
if super::commits::matches_key(key, &keybindings.commits.open_log_menu) {
if matches_key(key, &keybindings.commits.open_log_menu) {
let selected = gui.context_mgr.selected_active();
let selected_path = if gui.show_commit_file_tree {
gui.commit_file_tree_nodes
Expand Down Expand Up @@ -48,9 +48,7 @@ pub fn handle_key(gui: &mut Gui, key: KeyEvent, keybindings: &KeybindingConfig)
}

// Folding is explicit; Enter focuses file or combined directory diffs.
if gui.show_commit_file_tree
&& super::tree::matches_key(key, &keybindings.universal.fold_directory)
{
if gui.show_commit_file_tree && matches_key(key, &keybindings.universal.fold_directory) {
if let Some(node) = gui
.commit_file_tree_nodes
.get(gui.context_mgr.selected_active())
Expand Down Expand Up @@ -316,11 +314,3 @@ pub fn update_commit_file_tree_state(gui: &mut Gui) {
gui.context_mgr.commit_files_list_len_override = None;
}
}

fn matches_key(key: KeyEvent, binding: &str) -> bool {
if let Some(expected) = parse_key(binding) {
key.code == expected.code && key.modifiers == expected.modifiers
} else {
false
}
}
10 changes: 1 addition & 9 deletions src/gui/controller/commits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use anyhow::Result;
use crossterm::event::KeyEvent;

use crate::config::KeybindingConfig;
use crate::config::keybindings::parse_key;
use crate::config::matches_key;
use crate::git::rebase::RebaseAction;
use crate::gui::Gui;
use crate::gui::popup::{
Expand Down Expand Up @@ -1501,14 +1501,6 @@ fn enter_interactive_rebase(gui: &mut Gui) -> Result<()> {
Ok(())
}

pub(super) fn matches_key(key: KeyEvent, binding: &str) -> bool {
if let Some(expected) = parse_key(binding) {
key.code == expected.code && key.modifiers == expected.modifiers
} else {
false
}
}

#[cfg(test)]
mod tests {
use super::{branches_at_commit, checkout_branch_key, nonempty, reset_ref_display};
Expand Down
15 changes: 3 additions & 12 deletions src/gui/controller/diff_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::sync::atomic::Ordering;
use anyhow::Result;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};

use crate::config::keybindings::parse_key;
use crate::config::matches_key;
use crate::gui::modes::diff_mode::{CompareDiffSource, DiffModeFocus, DiffModeSelector};
use crate::gui::popup::{CommandEntry, CommandSection, MenuItem, PopupState};
use crate::gui::{DiffPayload, Gui, textarea_input};
Expand Down Expand Up @@ -419,9 +419,7 @@ fn handle_commit_files_key(gui: &mut Gui, key: KeyEvent) -> Result<()> {
return Ok(());
}

if gui.diff_mode.show_tree
&& super::tree::matches_key(key, &keybindings.universal.fold_directory)
{
if gui.diff_mode.show_tree && matches_key(key, &keybindings.universal.fold_directory) {
if let Some(node) = gui
.diff_mode
.tree_nodes
Expand Down Expand Up @@ -1197,17 +1195,10 @@ fn show_diff_mode_command_palette(gui: &mut Gui) {
};
}

fn matches_key(key: KeyEvent, binding: &str) -> bool {
if let Some(expected) = parse_key(binding) {
key.code == expected.code && key.modifiers == expected.modifiers
} else {
false
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::config::keybindings::parse_key;
use crate::config::{AppConfig, AppState, UserConfig};
use crate::git::GitCommands;
use std::path::PathBuf;
Expand Down
14 changes: 3 additions & 11 deletions src/gui/controller/files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use anyhow::Result;
use crossterm::event::{KeyCode, KeyEvent};

use crate::config::KeybindingConfig;
use crate::config::keybindings::parse_key;
use crate::config::matches_key;
use crate::gui::Gui;
use crate::gui::popup::{
CommitInputFocus, CommitInputKind, MenuItem, PopupState, make_commit_body_textarea,
Expand All @@ -15,12 +15,12 @@ pub fn handle_key(gui: &mut Gui, key: KeyEvent, keybindings: &KeybindingConfig)
if super::diff_grep::is_diff_grep_key(key) {
return super::diff_grep::open_diff_grep_picker(gui);
}
if super::commits::matches_key(key, &keybindings.commits.open_log_menu) {
if matches_key(key, &keybindings.commits.open_log_menu) {
return super::commits::show_files_filtering_menu(gui);
}

// Folding is explicit; Enter focuses file or combined directory diffs.
if gui.show_file_tree && super::tree::matches_key(key, &keybindings.universal.fold_directory) {
if gui.show_file_tree && matches_key(key, &keybindings.universal.fold_directory) {
if let Some(node) = gui.file_tree_nodes.get(gui.context_mgr.selected_active()) {
if node.is_dir {
let path = node.path.clone();
Expand Down Expand Up @@ -947,11 +947,3 @@ fn commit_with_editor(gui: &mut Gui) -> Result<()> {
};
Ok(())
}

fn matches_key(key: KeyEvent, binding: &str) -> bool {
if let Some(expected) = parse_key(binding) {
key.code == expected.code && key.modifiers == expected.modifiers
} else {
false
}
}
10 changes: 1 addition & 9 deletions src/gui/controller/reflog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use anyhow::Result;
use crossterm::event::KeyEvent;

use crate::config::KeybindingConfig;
use crate::config::keybindings::parse_key;
use crate::config::matches_key;
use crate::gui::Gui;
use crate::gui::popup::{MenuItem, PopupState};
use crate::os::platform::Platform;
Expand Down Expand Up @@ -187,11 +187,3 @@ fn copy_to_clipboard_menu(gui: &mut Gui) -> Result<()> {
}
Ok(())
}

fn matches_key(key: KeyEvent, binding: &str) -> bool {
if let Some(expected) = parse_key(binding) {
key.code == expected.code && key.modifiers == expected.modifiers
} else {
false
}
}
10 changes: 1 addition & 9 deletions src/gui/controller/remote_branches.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use anyhow::Result;
use crossterm::event::{KeyCode, KeyEvent};

use crate::config::KeybindingConfig;
use crate::config::keybindings::parse_key;
use crate::config::matches_key;
use crate::gui::Gui;
use crate::gui::context::ContextId;
use crate::gui::popup::PopupState;
Expand Down Expand Up @@ -160,11 +160,3 @@ fn delete_remote_branch(gui: &mut Gui) -> Result<()> {
}
Ok(())
}

fn matches_key(key: KeyEvent, binding: &str) -> bool {
if let Some(expected) = parse_key(binding) {
key.code == expected.code && key.modifiers == expected.modifiers
} else {
false
}
}
10 changes: 1 addition & 9 deletions src/gui/controller/remotes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use anyhow::Result;
use crossterm::event::{KeyCode, KeyEvent};

use crate::config::KeybindingConfig;
use crate::config::keybindings::parse_key;
use crate::config::matches_key;
use crate::gui::Gui;
use crate::gui::popup::{MenuItem, MessageKind, PopupState, make_textarea};

Expand Down Expand Up @@ -480,11 +480,3 @@ fn show_pull_menu(gui: &mut Gui) -> Result<()> {
};
Ok(())
}

fn matches_key(key: KeyEvent, binding: &str) -> bool {
if let Some(expected) = parse_key(binding) {
key.code == expected.code && key.modifiers == expected.modifiers
} else {
false
}
}
10 changes: 1 addition & 9 deletions src/gui/controller/stash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use anyhow::Result;
use crossterm::event::{KeyCode, KeyEvent};

use crate::config::KeybindingConfig;
use crate::config::keybindings::parse_key;
use crate::config::matches_key;
use crate::gui::Gui;
use crate::gui::popup::{MenuItem, PopupState, make_textarea};

Expand Down Expand Up @@ -214,11 +214,3 @@ fn rename_stash(gui: &mut Gui) -> Result<()> {
}
Ok(())
}

fn matches_key(key: KeyEvent, binding: &str) -> bool {
if let Some(expected) = parse_key(binding) {
key.code == expected.code && key.modifiers == expected.modifiers
} else {
false
}
}
10 changes: 1 addition & 9 deletions src/gui/controller/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use anyhow::Result;
use crossterm::event::{KeyCode, KeyEvent};

use crate::config::KeybindingConfig;
use crate::config::keybindings::parse_key;
use crate::config::matches_key;
use crate::gui::Gui;
use crate::gui::popup::{MenuItem, PopupState};
use crate::os::platform::Platform;
Expand Down Expand Up @@ -156,11 +156,3 @@ fn open_menu(gui: &mut Gui) -> Result<()> {
};
Ok(())
}

fn matches_key(key: KeyEvent, binding: &str) -> bool {
if let Some(expected) = parse_key(binding) {
key.code == expected.code && key.modifiers == expected.modifiers
} else {
false
}
}
18 changes: 1 addition & 17 deletions src/gui/controller/tree.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,9 @@
//! Shared tree shortcuts for sidebar and diff-focused navigation.
use crate::config::KeybindingConfig;
use crate::config::keybindings::parse_key;
pub use crate::config::matches_key;
use crate::model::file_tree::{self, NavigableTreeNode};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};

/// Outer Option means the key is claimed; inner Option means a destination exists.
/// Boundary keys are consumed too, so they cannot fall through to unrelated actions.
pub fn matches_key(mut key: KeyEvent, binding: &str) -> bool {
let Some(expected) = parse_key(binding) else {
return false;
};
// Shifted punctuation may arrive with SHIFT on real terminals.
// Do not strip CTRL/ALT or SHIFT for letters / explicit modifier bindings.
if expected.modifiers.is_empty()
&& matches!(expected.code, KeyCode::Char(c) if c.is_ascii_punctuation())
{
key.modifiers.remove(KeyModifiers::SHIFT);
}
key.code == expected.code && key.modifiers == expected.modifiers
}

pub fn destination<T: NavigableTreeNode>(
key: KeyEvent,
kb: &KeybindingConfig,
Expand Down
Loading
Loading