aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-08-21 21:09:52 +0300
committerGitHub <noreply@github.com>2026-08-21 21:09:52 +0300
commit2d5f91dd0c2b4c7aa0a8c1d0666b0b0645ec7f9f (patch)
tree5156cc00b174f3b1eaf497eb6892eea309829c4c /src
parentf72a5ef99e2fbfed49eb739c4fffb0c273656d0b (diff)
fix: match generated characters in prefix bindings (#3085)
refs #3079
Diffstat (limited to 'src')
-rw-r--r--src/app/input/navigate.rs120
1 files changed, 100 insertions, 20 deletions
diff --git a/src/app/input/navigate.rs b/src/app/input/navigate.rs
index 2cb657a7..8e3d7d6e 100644
--- a/src/app/input/navigate.rs
+++ b/src/app/input/navigate.rs
@@ -80,27 +80,14 @@ impl App {
return;
}
- if let Some(action) =
- non_indexed_action_for_key(&self.state, &raw_key, BindingDispatch::Prefix)
- {
- self.execute_prefix_key_action(action);
- return;
- }
-
- if let Some(binding) = command_for_key(&self.state, &raw_key, BindingDispatch::Prefix) {
- self.cancel_copy_mode_if_active();
- self.launch_custom_command(binding, ActionContext::Prefix);
- return;
- }
-
- if let Some(action) =
- indexed_navigation_action(&self.state, &raw_key, BindingDispatch::Prefix)
- {
- self.execute_prefix_key_action(action);
- return;
+ match prefix_binding_for_key(&self.state, &raw_key) {
+ Some(PrefixBindingMatch::Action(action)) => self.execute_prefix_key_action(action),
+ Some(PrefixBindingMatch::Command(binding)) => {
+ self.cancel_copy_mode_if_active();
+ self.launch_custom_command(binding, ActionContext::Prefix);
+ }
+ None => leave_command_mode(&mut self.state),
}
-
- leave_command_mode(&mut self.state);
}
fn execute_prefix_key_action(&mut self, action: NavigateAction) {
@@ -1185,6 +1172,43 @@ pub(crate) enum BindingDispatch {
Prefix,
}
+enum PrefixBindingMatch {
+ Action(NavigateAction),
+ Command(crate::config::CustomCommandKeybind),
+}
+
+fn prefix_binding_for_key(state: &AppState, key: &TerminalKey) -> Option<PrefixBindingMatch> {
+ exact_prefix_binding_for_key(state, key).or_else(|| {
+ generated_character_key(key)
+ .as_ref()
+ .and_then(|generated_key| exact_prefix_binding_for_key(state, generated_key))
+ })
+}
+
+fn exact_prefix_binding_for_key(state: &AppState, key: &TerminalKey) -> Option<PrefixBindingMatch> {
+ non_indexed_action_for_key(state, key, BindingDispatch::Prefix)
+ .map(PrefixBindingMatch::Action)
+ .or_else(|| {
+ command_for_key(state, key, BindingDispatch::Prefix).map(PrefixBindingMatch::Command)
+ })
+ .or_else(|| {
+ indexed_navigation_action(state, key, BindingDispatch::Prefix)
+ .map(PrefixBindingMatch::Action)
+ })
+}
+
+fn generated_character_key(key: &TerminalKey) -> Option<TerminalKey> {
+ let mut characters = key.generated_text.as_deref()?.chars();
+ let character = characters.next()?;
+ if character.is_control() || characters.next().is_some() {
+ return None;
+ }
+ Some(TerminalKey::new(
+ KeyCode::Char(character),
+ crossterm::event::KeyModifiers::empty(),
+ ))
+}
+
pub(crate) fn command_for_key(
state: &AppState,
key: &TerminalKey,
@@ -2817,6 +2841,62 @@ resize_pane_left = "prefix+shift+left"
}
#[test]
+ fn generated_character_prefix_binding_falls_back_after_exact_chord() {
+ let generated_key = crate::input::parse_terminal_key_sequence("\x1b[119;3;124u").unwrap();
+ assert_eq!(generated_key.code, KeyCode::Char('w'));
+ assert_eq!(generated_key.modifiers, KeyModifiers::ALT);
+ assert_eq!(generated_key.generated_text.as_deref(), Some("|"));
+
+ let generated_only: Config = toml::from_str(
+ r#"
+[keys]
+split_vertical = "prefix+|"
+"#,
+ )
+ .unwrap();
+ let mut state = state_with_workspaces(&["test"]);
+ state.keybinds = generated_only.keybinds();
+ assert!(matches!(
+ prefix_binding_for_key(&state, &generated_key),
+ Some(PrefixBindingMatch::Action(NavigateAction::SplitVertical))
+ ));
+ let multi_character_key =
+ crate::input::parse_terminal_key_sequence("\x1b[119;3;124:120u").unwrap();
+ assert!(prefix_binding_for_key(&state, &multi_character_key).is_none());
+
+ let exact_and_generated: Config = toml::from_str(
+ r#"
+[keys]
+split_vertical = "prefix+|"
+split_horizontal = "prefix+alt+w"
+"#,
+ )
+ .unwrap();
+ state.keybinds = exact_and_generated.keybinds();
+ assert!(matches!(
+ prefix_binding_for_key(&state, &generated_key),
+ Some(PrefixBindingMatch::Action(NavigateAction::SplitHorizontal))
+ ));
+
+ let exact_command: Config = toml::from_str(
+ r#"
+[keys]
+split_vertical = "prefix+|"
+
+[[keys.command]]
+key = "prefix+alt+w"
+command = "echo exact"
+"#,
+ )
+ .unwrap();
+ state.keybinds = exact_command.keybinds();
+ assert!(matches!(
+ prefix_binding_for_key(&state, &generated_key),
+ Some(PrefixBindingMatch::Command(binding)) if binding.command == "echo exact"
+ ));
+ }
+
+ #[test]
fn shifted_backslash_layout_prefers_horizontal_split_binding() {
let config: Config = toml::from_str(
r#"