aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-03 02:39:12 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-03 02:39:39 +0300
commit8968a0a4a8e80761ae6c8dab9b912530f619479c (patch)
tree5b4b8ebe1b41d51d3da70715425002d3449db036 /src
parent719dff6d3cefbe586403d5a8454b119df4ec3ccd (diff)
fix: support legacy shifted letter keybindings
refs #312
Diffstat (limited to 'src')
-rw-r--r--src/app/input/navigate.rs82
-rw-r--r--src/config/keybinds.rs86
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!(