diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-03 02:39:12 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-03 02:39:39 +0300 |
| commit | 8968a0a4a8e80761ae6c8dab9b912530f619479c (patch) | |
| tree | 5b4b8ebe1b41d51d3da70715425002d3449db036 /src | |
| parent | 719dff6d3cefbe586403d5a8454b119df4ec3ccd (diff) | |
fix: support legacy shifted letter keybindings
refs #312
Diffstat (limited to 'src')
| -rw-r--r-- | src/app/input/navigate.rs | 82 | ||||
| -rw-r--r-- | src/config/keybinds.rs | 86 |
2 files changed, 165 insertions, 3 deletions
diff --git a/src/app/input/navigate.rs b/src/app/input/navigate.rs index b6edefaf..4bd69f1c 100644 --- a/src/app/input/navigate.rs +++ b/src/app/input/navigate.rs @@ -1570,6 +1570,88 @@ last_pane = "prefix+tab" } #[tokio::test] + async fn navigate_mode_matches_legacy_uppercase_shifted_letter() { + let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel(); + let mut app = App::new( + &Config::default(), + true, + None, + api_rx, + crate::api::EventHub::default(), + ); + app.state.workspaces = vec![Workspace::test_new("test")]; + app.state.active = Some(0); + app.state.selected = 0; + app.state.mode = Mode::Navigate; + + app.handle_navigate_key(TerminalKey::new(KeyCode::Char('N'), KeyModifiers::empty())); + + assert!(app.state.request_new_workspace); + assert_eq!(app.state.mode, Mode::Terminal); + } + + #[tokio::test] + async fn legacy_uppercase_prefers_shifted_workspace_binding_over_unshifted() { + let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel(); + let mut app = App::new( + &Config::default(), + true, + None, + api_rx, + crate::api::EventHub::default(), + ); + app.state.workspaces = vec![Workspace::test_new("test")]; + app.state.active = Some(0); + app.state.selected = 0; + app.state.mode = Mode::Navigate; + + app.handle_navigate_key(TerminalKey::new(KeyCode::Char('W'), KeyModifiers::empty())); + + assert_eq!(app.state.mode, Mode::RenameWorkspace); + } + + #[tokio::test] + async fn legacy_uppercase_prefers_shifted_reload_binding_over_unshifted() { + let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel(); + let mut app = App::new( + &Config::default(), + true, + None, + api_rx, + crate::api::EventHub::default(), + ); + app.state.workspaces = vec![Workspace::test_new("test")]; + app.state.active = Some(0); + app.state.selected = 0; + app.state.mode = Mode::Navigate; + + app.handle_navigate_key(TerminalKey::new(KeyCode::Char('R'), KeyModifiers::empty())); + + assert!(app.state.request_reload_config); + assert_eq!(app.state.mode, Mode::Terminal); + } + + #[tokio::test] + async fn legacy_uppercase_prefers_shifted_pane_binding_over_unshifted() { + let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel(); + let mut app = App::new( + &Config::default(), + true, + None, + api_rx, + crate::api::EventHub::default(), + ); + app.state.workspaces = vec![Workspace::test_new("test")]; + app.state.active = Some(0); + app.state.selected = 0; + app.state.mode = Mode::Navigate; + + app.handle_navigate_key(TerminalKey::new(KeyCode::Char('P'), KeyModifiers::empty())); + + assert_eq!(app.state.mode, Mode::RenamePane); + } + + #[tokio::test] async fn prefix_focus_pane_is_one_shot_and_returns_to_terminal() { let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel(); let mut app = App::new( diff --git a/src/config/keybinds.rs b/src/config/keybinds.rs index 6ae8c4a1..299af6cb 100644 --- a/src/config/keybinds.rs +++ b/src/config/keybinds.rs @@ -1075,7 +1075,13 @@ fn key_parts_match_combo( let (expected_code, expected_modifiers) = normalize_key_combo(combo); if actual_modifiers == expected_modifiers - && key_codes_match(actual_code, expected_code, shifted_codepoint) + && key_codes_match( + actual_code, + actual_modifiers, + expected_code, + expected_modifiers, + shifted_codepoint, + ) { return true; } @@ -1084,14 +1090,29 @@ fn key_parts_match_combo( actual_modifiers.contains(KeyModifiers::SHIFT) && actual_without_shift == expected_modifiers && shifted_char_matches_expected(actual_code, shifted_codepoint, expected_code) + || legacy_shifted_ascii_letter_matches( + actual_code, + actual_modifiers, + expected_code, + expected_modifiers, + ) } -fn key_codes_match(actual: KeyCode, expected: KeyCode, shifted_codepoint: Option<u32>) -> bool { +fn key_codes_match( + actual: KeyCode, + actual_modifiers: KeyModifiers, + expected: KeyCode, + expected_modifiers: KeyModifiers, + shifted_codepoint: Option<u32>, +) -> bool { match (actual, expected) { (KeyCode::Char(actual), KeyCode::Char(expected)) if actual.is_ascii_alphabetic() && expected.is_ascii_alphabetic() => { - actual.eq_ignore_ascii_case(&expected) + actual == expected + || actual_modifiers.contains(KeyModifiers::SHIFT) + && expected_modifiers.contains(KeyModifiers::SHIFT) + && actual.eq_ignore_ascii_case(&expected) } (KeyCode::Char(actual), KeyCode::Char(expected)) => { actual == expected @@ -1105,6 +1126,24 @@ fn key_codes_match(actual: KeyCode, expected: KeyCode, shifted_codepoint: Option } } +fn legacy_shifted_ascii_letter_matches( + actual_code: KeyCode, + actual_modifiers: KeyModifiers, + expected_code: KeyCode, + expected_modifiers: KeyModifiers, +) -> bool { + if actual_modifiers.contains(KeyModifiers::SHIFT) { + return false; + } + let (KeyCode::Char(actual), KeyCode::Char(expected)) = (actual_code, expected_code) else { + return false; + }; + actual.is_ascii_uppercase() + && expected.is_ascii_lowercase() + && actual.to_ascii_lowercase() == expected + && actual_modifiers | KeyModifiers::SHIFT == expected_modifiers +} + fn shifted_char_matches_expected( actual_code: KeyCode, shifted_codepoint: Option<u32>, @@ -1346,6 +1385,47 @@ close_tab = "X" } #[test] + fn shifted_letter_binding_matches_legacy_uppercase_key_event() { + let bindings = ActionKeybinds::prefix("shift+n"); + assert!(bindings + .matches_prefix_key(TerminalKey::new(KeyCode::Char('N'), KeyModifiers::empty(),))); + } + + #[test] + fn shifted_letter_direct_binding_matches_legacy_uppercase_key_event() { + let bindings = ActionKeybinds::direct("shift+n"); + assert!(bindings + .matches_direct_key(TerminalKey::new(KeyCode::Char('N'), KeyModifiers::empty(),))); + } + + #[test] + fn shifted_letter_binding_matches_modern_modified_key_event() { + let bindings = ActionKeybinds::direct("cmd+shift+j"); + assert!(bindings.matches_direct_key(TerminalKey::new( + KeyCode::Char('J'), + KeyModifiers::SUPER | KeyModifiers::SHIFT, + ))); + } + + #[test] + fn legacy_uppercase_key_event_does_not_match_unshifted_letter_binding() { + let bindings = ActionKeybinds::prefix("n"); + assert!(!bindings + .matches_prefix_key(TerminalKey::new(KeyCode::Char('N'), KeyModifiers::empty(),))); + } + + #[test] + fn legacy_uppercase_shift_fallback_is_limited_to_ascii_letters() { + let shifted_number = ActionKeybinds::prefix("shift+1"); + assert!(!shifted_number + .matches_prefix_key(TerminalKey::new(KeyCode::Char('!'), KeyModifiers::empty(),))); + + let shifted_non_ascii = ActionKeybinds::prefix("shift+ö"); + assert!(!shifted_non_ascii + .matches_prefix_key(TerminalKey::new(KeyCode::Char('Ö'), KeyModifiers::empty(),))); + } + + #[test] fn shifted_tab_inputs_match_backtab_canonical_binding() { let bindings = ActionKeybinds::prefix("shift+tab"); assert!( |
