From e536bd8bd99de975cb2d15bb384e6e35f88ef88e Mon Sep 17 00:00:00 2001 From: Can Celik Date: Sun, 26 Jul 2026 04:22:05 +0300 Subject: fix: support non-us shifted keybindings (#1876) * fix: support non-us shifted keybindings refs #1870 * docs: require current pull request base --- src/app/input/navigate.rs | 136 +++++++++++++++++++++++++++++++++++----------- src/app/input/terminal.rs | 6 ++ src/app/mod.rs | 6 +- 3 files changed, 115 insertions(+), 33 deletions(-) (limited to 'src/app') diff --git a/src/app/input/navigate.rs b/src/app/input/navigate.rs index a5300461..6b578091 100644 --- a/src/app/input/navigate.rs +++ b/src/app/input/navigate.rs @@ -1131,8 +1131,29 @@ pub(crate) fn command_for_key( .cloned() } +fn unmodified_digit_for_key(key: TerminalKey) -> Option { + ('1'..='9').find(|digit| { + crate::config::terminal_key_matches_combo( + key, + ( + KeyCode::Char(*digit), + crossterm::event::KeyModifiers::empty(), + ), + ) + }) +} + #[cfg(test)] pub(super) fn handle_navigate_reserved_key(state: &mut AppState, key: TerminalKey) -> bool { + if let Some(c) = unmodified_digit_for_key(key) { + let idx = (c as usize) - ('1' as usize); + if let Some(ws_idx) = state.workspace_at_visible_position(idx) { + state.switch_workspace(ws_idx); + leave_navigate_mode(state); + } + return true; + } + let (code, modifiers) = crate::config::normalize_key_combo((key.code, key.modifiers)); if modifiers.is_empty() { match code { @@ -1143,14 +1164,6 @@ pub(super) fn handle_navigate_reserved_key(state: &mut AppState, key: TerminalKe } return true; } - KeyCode::Char(c @ '1'..='9') => { - let idx = (c as usize) - ('1' as usize); - if let Some(ws_idx) = state.workspace_at_visible_position(idx) { - state.switch_workspace(ws_idx); - leave_navigate_mode(state); - } - return true; - } KeyCode::Tab => { state.cycle_pane(false); return true; @@ -1205,6 +1218,12 @@ pub(super) fn handle_navigate_reserved_key(state: &mut AppState, key: TerminalKe } fn navigate_reserved_action_for_key(state: &AppState, key: TerminalKey) -> Option { + if let Some(c) = unmodified_digit_for_key(key) { + return Some(NavigateAction::SwitchWorkspace( + (c as usize) - ('1' as usize), + )); + } + let (code, modifiers) = crate::config::normalize_key_combo((key.code, key.modifiers)); if modifiers.is_empty() { match code { @@ -1217,11 +1236,6 @@ fn navigate_reserved_action_for_key(state: &AppState, key: TerminalKey) -> Optio .unwrap_or(state.selected), )); } - KeyCode::Char(c @ '1'..='9') => { - return Some(NavigateAction::SwitchWorkspace( - (c as usize) - ('1' as usize), - )); - } KeyCode::Tab => return Some(NavigateAction::CyclePaneNext), KeyCode::BackTab => return Some(NavigateAction::CyclePanePrevious), KeyCode::Left => return Some(NavigateAction::FocusPaneLeft), @@ -1367,29 +1381,37 @@ fn indexed_navigation_action( dispatch: BindingDispatch, ) -> Option { let kb = &state.keybinds; - let trigger_matches = |binding: &crate::config::IndexedKeybind| match dispatch { - BindingDispatch::Direct => binding.trigger.is_direct(), - BindingDispatch::Prefix => binding.trigger.is_prefix(), - }; + let actual_modifiers = crate::config::normalize_key_combo((key.code, key.modifiers)).1; + + for exact_modifiers in [true, false] { + let trigger_matches = |binding: &crate::config::IndexedKeybind| { + let dispatch_matches = match dispatch { + BindingDispatch::Direct => binding.trigger.is_direct(), + BindingDispatch::Prefix => binding.trigger.is_prefix(), + }; + let expected_modifiers = crate::config::normalize_key_combo(binding.trigger.combo()).1; + dispatch_matches && (actual_modifiers == expected_modifiers) == exact_modifiers + }; - for binding in &kb.switch_tab { - if trigger_matches(binding) { - if let Some(idx) = binding.matched_index(key) { - return Some(NavigateAction::SwitchTab(idx)); + for binding in &kb.switch_tab { + if trigger_matches(binding) { + if let Some(idx) = binding.matched_index(key) { + return Some(NavigateAction::SwitchTab(idx)); + } } } - } - for binding in &kb.switch_workspace { - if trigger_matches(binding) { - if let Some(idx) = binding.matched_index(key) { - return Some(NavigateAction::SwitchWorkspace(idx)); + for binding in &kb.switch_workspace { + if trigger_matches(binding) { + if let Some(idx) = binding.matched_index(key) { + return Some(NavigateAction::SwitchWorkspace(idx)); + } } } - } - for binding in &kb.focus_agent { - if trigger_matches(binding) { - if let Some(idx) = binding.matched_index(key) { - return Some(NavigateAction::FocusAgent(idx)); + for binding in &kb.focus_agent { + if trigger_matches(binding) { + if let Some(idx) = binding.matched_index(key) { + return Some(NavigateAction::FocusAgent(idx)); + } } } } @@ -2564,7 +2586,7 @@ last_pane = "prefix+tab" } #[test] - fn prefix_shift_indexed_workspace_shortcut_maps_shifted_symbol_key() { + fn prefix_shift_indexed_workspace_shortcut_maps_legacy_us_symbol_key() { let mut state = state_with_workspaces(&["one", "two"]); let config: Config = toml::from_str("[keys]\nswitch_workspace = \"prefix+shift+1..9\"\n").unwrap(); @@ -2579,6 +2601,42 @@ last_pane = "prefix+tab" assert_eq!(action, Some(NavigateAction::SwitchWorkspace(1))); } + #[test] + fn prefix_shift_indexed_workspace_shortcut_maps_non_us_number_rows() { + let mut state = state_with_workspaces(&["one", "two"]); + let config: Config = + toml::from_str("[keys]\nswitch_workspace = \"prefix+shift+1..9\"\n").unwrap(); + state.keybinds.switch_workspace = config.keybinds().switch_workspace; + + for key in [ + TerminalKey::new(KeyCode::Char('2'), KeyModifiers::SHIFT) + .with_shifted_codepoint('"' as u32), + TerminalKey::new(KeyCode::Char('é'), KeyModifiers::SHIFT) + .with_shifted_codepoint('2' as u32), + ] { + assert_eq!( + action_for_key(&state, key, BindingDispatch::Prefix), + Some(NavigateAction::SwitchWorkspace(1)) + ); + } + } + + #[test] + fn prefix_unshifted_indexed_shortcut_maps_shifted_french_number_row() { + let mut state = state_with_workspaces(&["one"]); + let config: Config = toml::from_str("[keys]\nswitch_tab = \"prefix+1..9\"\n").unwrap(); + state.keybinds.switch_tab = config.keybinds().switch_tab; + + let action = action_for_key( + &state, + TerminalKey::new(KeyCode::Char('é'), KeyModifiers::SHIFT) + .with_shifted_codepoint('2' as u32), + BindingDispatch::Prefix, + ); + + assert_eq!(action, Some(NavigateAction::SwitchTab(1))); + } + #[test] fn literal_symbol_binding_takes_precedence_over_shifted_indexed_alias() { let mut state = state_with_workspaces(&["one", "two"]); @@ -2784,6 +2842,20 @@ command = "printf literal > '{}'" assert_eq!(app.state.mode, Mode::Navigate); } + #[test] + fn app_navigate_mode_maps_french_number_row_to_workspace() { + let mut app = app_with_test_workspaces(&["one", "two"]); + app.state.mode = Mode::Navigate; + + app.handle_navigate_key( + TerminalKey::new(KeyCode::Char('é'), KeyModifiers::SHIFT) + .with_shifted_codepoint('2' as u32), + ); + + assert_eq!(app.state.active, Some(1)); + assert_eq!(app.state.mode, Mode::Terminal); + } + #[test] fn app_navigate_mode_workspace_keys_are_configurable() { let mut app = app_with_test_workspaces(&["one", "two"]); diff --git a/src/app/input/terminal.rs b/src/app/input/terminal.rs index d862702f..a6c3abcb 100644 --- a/src/app/input/terminal.rs +++ b/src/app/input/terminal.rs @@ -264,6 +264,12 @@ impl App { } pub(crate) fn host_keyboard_report_all_requested(&self) -> bool { + if self.state.popup_pane.is_none() + && matches!(self.state.mode, Mode::Prefix | Mode::Navigate) + { + return true; + } + let runtime = if self.state.popup_pane.is_some() { self.popup_runtime() } else if self.state.mode == Mode::Terminal { diff --git a/src/app/mod.rs b/src/app/mod.rs index e9aa24b6..dc6de136 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -5064,7 +5064,7 @@ last_pane = "prefix+tab" } #[tokio::test] - async fn host_report_all_follows_the_focused_terminal_protocol() { + async fn host_report_all_follows_terminal_protocol_and_command_modes() { let mut app = test_app(); let mut workspace = Workspace::test_new("test"); let focused = workspace.focused_pane_id().unwrap(); @@ -5099,6 +5099,10 @@ last_pane = "prefix+tab" assert!(app.state.focus_pane_in_workspace(0, focused)); app.state.mode = Mode::Prefix; + assert!(app.host_keyboard_report_all_requested()); + app.state.mode = Mode::Navigate; + assert!(app.host_keyboard_report_all_requested()); + app.state.mode = Mode::RenameWorkspace; assert!(!app.host_keyboard_report_all_requested()); } -- cgit v1.2.3