aboutsummaryrefslogtreecommitdiff
path: root/src/app
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-07-26 04:22:05 +0300
committerGitHub <noreply@github.com>2026-07-26 04:22:05 +0300
commite536bd8bd99de975cb2d15bb384e6e35f88ef88e (patch)
tree4de9e0d10c7b0727e693f70b4dbd7859da45f24d /src/app
parent8843bbb0c4dc4e6b52ca5d0a6592ce80be56a19c (diff)
fix: support non-us shifted keybindings (#1876)
* fix: support non-us shifted keybindings refs #1870 * docs: require current pull request base
Diffstat (limited to 'src/app')
-rw-r--r--src/app/input/navigate.rs136
-rw-r--r--src/app/input/terminal.rs6
-rw-r--r--src/app/mod.rs6
3 files changed, 115 insertions, 33 deletions
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<char> {
+ ('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<NavigateAction> {
+ 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<NavigateAction> {
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();
@@ -2580,6 +2602,42 @@ last_pane = "prefix+tab"
}
#[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"]);
let config: Config = toml::from_str(
@@ -2785,6 +2843,20 @@ command = "printf literal > '{}'"
}
#[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"]);
let config: Config = toml::from_str(
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());
}