fix: match generated characters in prefix bindings

refs #3079
This commit is contained in:
Ogulcan Celik
2026-08-21 17:41:18 +03:00
parent 8fe09d5ddf
commit 2df3fbf66a
2 changed files with 101 additions and 20 deletions
+1
View File
@@ -3,6 +3,7 @@
## Unreleased
### Fixed
- Prefix bindings such as `prefix+|` now recognize characters produced by macOS Option and custom keyboard layouts, while exact chords such as `prefix+alt+w` keep priority. (#3079, thanks @vlcinsky)
- Direct terminal attaches now preserve multiline pastes as one paste instead of submitting each line separately. (#3054)
- Windows panes now keep bare `cursor-agent` launches detected after Cursor hands off to its bundled Node process. (#3032)
- Oversized Kitty images no longer prevent smaller images shown later in the same pane from rendering. (#3033)
+100 -20
View File
@@ -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;
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),
}
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;
}
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,
@@ -2816,6 +2840,62 @@ resize_pane_left = "prefix+shift+left"
assert_eq!(action, Some(NavigateAction::LastPane));
}
#[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(