mirror of
https://github.com/fish-shell/fish-shell.git
synced 2026-05-27 00:21:15 -03:00
kitty keyboard protocol: fall back to base layout key
On terminals that do not implement the kitty keyboard protocol "ctrl-ц" on
a Russian keyboard layout generally sends the same byte as "ctrl-w". This
is because historically there was no standard way to encode "ctrl-ц",
and the "ц" letter happens to be in the same position as "w" on the PC-101
keyboard layout.
Users have gotten used to this, probably because many of them are switching
between a Russian (or Greek etc.) and an English layout.
Vim/Emacs allow opting in to this behavior by setting the "input method"
(which probably means "keyboard layout").
Match key events that have the base layout key set against bindings for
that key.
Closes #11520
---
Alternatively, we could add the relevant preset bindings (for "ctrl-ц" etc.)
but
1. this will be wrong if there is a disagreement on the placement of "ц" between two layouts
2. there are a lot of them
3. it won't work for user bindings (for better or worse)
(cherry picked from commit 7a79728df3)
This commit is contained in:
@@ -122,6 +122,11 @@ fn process_input(continuous_mode: bool, verbose: bool) -> i32 {
|
||||
shifted_key.codepoint = kevt.key.shifted_codepoint;
|
||||
keys.push((shifted_key, "shifted key"));
|
||||
}
|
||||
if kevt.key.base_layout_codepoint != '\0' {
|
||||
let mut base_layout_key = kevt.key.key;
|
||||
base_layout_key.codepoint = kevt.key.base_layout_codepoint;
|
||||
keys.push((base_layout_key, "physical key"));
|
||||
}
|
||||
for (key, explanation) in keys {
|
||||
printf!(
|
||||
"bind %s 'do something'%s%s\n",
|
||||
|
||||
@@ -143,20 +143,23 @@ pub enum ReadlineCmd {
|
||||
pub struct KeyEvent {
|
||||
pub key: Key,
|
||||
pub shifted_codepoint: char,
|
||||
pub base_layout_codepoint: char,
|
||||
}
|
||||
|
||||
impl KeyEvent {
|
||||
pub(crate) fn new(modifiers: Modifiers, codepoint: char) -> Self {
|
||||
Self::from(Key::new(modifiers, codepoint))
|
||||
}
|
||||
pub(crate) fn with_shifted_codepoint(
|
||||
pub(crate) fn new_with(
|
||||
modifiers: Modifiers,
|
||||
codepoint: char,
|
||||
shifted_codepoint: Option<char>,
|
||||
shifted_key: Option<char>,
|
||||
base_layout_key: Option<char>,
|
||||
) -> Self {
|
||||
Self {
|
||||
key: Key::new(modifiers, codepoint),
|
||||
shifted_codepoint: shifted_codepoint.unwrap_or_default(),
|
||||
shifted_codepoint: shifted_key.unwrap_or_default(),
|
||||
base_layout_codepoint: base_layout_key.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
pub(crate) fn from_raw(codepoint: char) -> Self {
|
||||
@@ -169,10 +172,7 @@ pub fn from_single_byte(c: u8) -> Self {
|
||||
|
||||
impl From<Key> for KeyEvent {
|
||||
fn from(key: Key) -> Self {
|
||||
Self {
|
||||
key,
|
||||
shifted_codepoint: '\0',
|
||||
}
|
||||
Self::new_with(key.modifiers, key.codepoint, None, None)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,6 +211,8 @@ fn apply_shift(mut key: Key, do_ascii: bool, shifted_codepoint: char) -> Option<
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum KeyMatchQuality {
|
||||
Legacy,
|
||||
BaseLayoutModuloShift,
|
||||
BaseLayout,
|
||||
ModuloShift,
|
||||
Exact,
|
||||
}
|
||||
@@ -222,9 +224,25 @@ pub fn match_key_event_to_key(event: &KeyEvent, key: &Key) -> Option<KeyMatchQua
|
||||
return Some(KeyMatchQuality::Exact);
|
||||
}
|
||||
|
||||
let shifted_evt = apply_shift(event.key, false, event.shifted_codepoint)?;
|
||||
let shifted_key = apply_shift(*key, true, '\0')?;
|
||||
(shifted_evt == shifted_key).then_some(KeyMatchQuality::ModuloShift)
|
||||
let shifted_evt = apply_shift(event.key, false, event.shifted_codepoint);
|
||||
let shifted_key = apply_shift(*key, true, '\0');
|
||||
if shifted_evt.is_some() && shifted_evt == shifted_key {
|
||||
return Some(KeyMatchQuality::ModuloShift);
|
||||
}
|
||||
|
||||
if event.base_layout_codepoint != '\0' {
|
||||
let mut base_layout_key = event.key;
|
||||
base_layout_key.codepoint = event.base_layout_codepoint;
|
||||
if base_layout_key == *key {
|
||||
return Some(KeyMatchQuality::BaseLayout);
|
||||
}
|
||||
let shifted_base_layout_key = apply_shift(base_layout_key, true, '\0');
|
||||
if shifted_base_layout_key.is_some() && shifted_base_layout_key == shifted_key {
|
||||
return Some(KeyMatchQuality::BaseLayoutModuloShift);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -246,6 +264,8 @@ macro_rules! validate {
|
||||
|
||||
let exact = KeyMatchQuality::Exact;
|
||||
let modulo_shift = KeyMatchQuality::ModuloShift;
|
||||
let base_layout = KeyMatchQuality::BaseLayout;
|
||||
let base_layout_modulo_shift = KeyMatchQuality::BaseLayoutModuloShift;
|
||||
|
||||
validate!(KeyEvent::new(none, 'a'), Key::new(none, 'a'), Some(exact));
|
||||
validate!(KeyEvent::new(none, 'a'), Key::new(none, 'A'), None);
|
||||
@@ -266,7 +286,7 @@ macro_rules! validate {
|
||||
// FYI: for codepoints that are not letters with uppercase/lowercase versions, we use
|
||||
// the shifted key in the canonical notation, because the unshifted one may depend on the
|
||||
// keyboard layout.
|
||||
let ctrl_shift_equals = KeyEvent::with_shifted_codepoint(ctrl_shift, '=', Some('+'));
|
||||
let ctrl_shift_equals = KeyEvent::new_with(ctrl_shift, '=', Some('+'), None);
|
||||
validate!(ctrl_shift_equals, Key::new(ctrl_shift, '='), Some(exact));
|
||||
validate!(ctrl_shift_equals, Key::new(ctrl, '+'), Some(modulo_shift)); // canonical notation
|
||||
validate!(ctrl_shift_equals, Key::new(ctrl_shift, '+'), None);
|
||||
@@ -281,11 +301,30 @@ macro_rules! validate {
|
||||
validate!(caps_ctrl_shift_ä, Key::new(ctrl, 'Ä'), None); // can't match without shifted key
|
||||
validate!(caps_ctrl_shift_ä, Key::new(ctrl_shift, 'Ä'), None);
|
||||
// With a shifted codepoint, we can match the alternative notation too.
|
||||
let caps_ctrl_shift_ä = KeyEvent::with_shifted_codepoint(ctrl_shift, 'ä', Some('Ä'));
|
||||
let caps_ctrl_shift_ä = KeyEvent::new_with(ctrl_shift, 'ä', Some('Ä'), None);
|
||||
validate!(caps_ctrl_shift_ä, Key::new(ctrl_shift, 'ä'), Some(exact)); // canonical notation
|
||||
validate!(caps_ctrl_shift_ä, Key::new(ctrl, 'ä'), None);
|
||||
validate!(caps_ctrl_shift_ä, Key::new(ctrl, 'Ä'), Some(modulo_shift)); // matched via shifted key
|
||||
validate!(caps_ctrl_shift_ä, Key::new(ctrl_shift, 'Ä'), None);
|
||||
|
||||
let ctrl_ц = KeyEvent::new_with(ctrl, 'ц', None, Some('w'));
|
||||
let ctrl_shift_ц = KeyEvent::new_with(ctrl_shift, 'ц', Some('Ц'), Some('w'));
|
||||
validate!(ctrl_ц, Key::new(ctrl, 'ц'), Some(exact));
|
||||
validate!(ctrl_ц, Key::new(ctrl, 'w'), Some(base_layout));
|
||||
validate!(ctrl_ц, Key::new(ctrl_shift, 'ц'), None);
|
||||
validate!(ctrl_ц, Key::new(ctrl_shift, 'w'), None);
|
||||
validate!(
|
||||
ctrl_shift_ц,
|
||||
Key::new(ctrl, 'W'),
|
||||
Some(base_layout_modulo_shift)
|
||||
);
|
||||
validate!(ctrl_shift_ц, Key::new(ctrl, 'w'), None);
|
||||
|
||||
// Note that "bind ctrl-Ц" will win over "bind ctrl-shift-w".
|
||||
// This is because we consider shift transformation to be less magic than base-key
|
||||
// transformation.
|
||||
validate!(ctrl_shift_ц, Key::new(ctrl, 'Ц'), Some(modulo_shift));
|
||||
validate!(ctrl_shift_ц, Key::new(ctrl_shift, 'w'), Some(base_layout));
|
||||
}
|
||||
|
||||
/// Represents an event on the character input stream.
|
||||
@@ -988,7 +1027,7 @@ fn parse_csi(&mut self, buffer: &mut Vec<u8>) -> Option<KeyEvent> {
|
||||
return None;
|
||||
}
|
||||
|
||||
let masked_key = |codepoint: char, shifted_codepoint: Option<char>| {
|
||||
let kitty_key = |key: char, shifted_key: Option<char>, base_layout_key: Option<char>| {
|
||||
let mask = params[1][0].saturating_sub(1);
|
||||
let (mut modifiers, caps_lock) = parse_mask(mask);
|
||||
|
||||
@@ -1012,12 +1051,13 @@ fn parse_csi(&mut self, buffer: &mut Vec<u8>) -> Option<KeyEvent> {
|
||||
// match the "shift-ä" event, as suggested in the kitty issue.
|
||||
if caps_lock
|
||||
&& modifiers == Modifiers::SHIFT
|
||||
&& !codepoint.to_uppercase().eq(Some(codepoint).into_iter())
|
||||
&& !key.to_uppercase().eq(Some(key).into_iter())
|
||||
{
|
||||
modifiers.shift = false;
|
||||
}
|
||||
KeyEvent::with_shifted_codepoint(modifiers, codepoint, shifted_codepoint)
|
||||
KeyEvent::new_with(modifiers, key, shifted_key, base_layout_key)
|
||||
};
|
||||
let masked_key = |key: char| kitty_key(key, None, None);
|
||||
|
||||
let key = match c {
|
||||
b'$' => {
|
||||
@@ -1032,13 +1072,13 @@ fn parse_csi(&mut self, buffer: &mut Vec<u8>) -> Option<KeyEvent> {
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
b'A' => masked_key(key::Up, None),
|
||||
b'B' => masked_key(key::Down, None),
|
||||
b'C' => masked_key(key::Right, None),
|
||||
b'D' => masked_key(key::Left, None),
|
||||
b'E' => masked_key('5', None), // Numeric keypad
|
||||
b'F' => masked_key(key::End, None), // PC/xterm style
|
||||
b'H' => masked_key(key::Home, None), // PC/xterm style
|
||||
b'A' => masked_key(key::Up),
|
||||
b'B' => masked_key(key::Down),
|
||||
b'C' => masked_key(key::Right),
|
||||
b'D' => masked_key(key::Left),
|
||||
b'E' => masked_key('5'), // Numeric keypad
|
||||
b'F' => masked_key(key::End), // PC/xterm style
|
||||
b'H' => masked_key(key::Home), // PC/xterm style
|
||||
b'M' | b'm' => {
|
||||
self.disable_mouse_tracking();
|
||||
let sgr = private_mode == Some(b'<');
|
||||
@@ -1074,30 +1114,27 @@ fn parse_csi(&mut self, buffer: &mut Vec<u8>) -> Option<KeyEvent> {
|
||||
}
|
||||
return None;
|
||||
}
|
||||
b'P' => masked_key(function_key(1), None),
|
||||
b'Q' => masked_key(function_key(2), None),
|
||||
b'R' => masked_key(function_key(3), None),
|
||||
b'S' => masked_key(function_key(4), None),
|
||||
b'P' => masked_key(function_key(1)),
|
||||
b'Q' => masked_key(function_key(2)),
|
||||
b'R' => masked_key(function_key(3)),
|
||||
b'S' => masked_key(function_key(4)),
|
||||
b'~' => match params[0][0] {
|
||||
1 => masked_key(key::Home, None), // VT220/tmux style
|
||||
2 => masked_key(key::Insert, None),
|
||||
3 => masked_key(key::Delete, None),
|
||||
4 => masked_key(key::End, None), // VT220/tmux style
|
||||
5 => masked_key(key::PageUp, None),
|
||||
6 => masked_key(key::PageDown, None),
|
||||
7 => masked_key(key::Home, None), // rxvt style
|
||||
8 => masked_key(key::End, None), // rxvt style
|
||||
1 => masked_key(key::Home), // VT220/tmux style
|
||||
2 => masked_key(key::Insert),
|
||||
3 => masked_key(key::Delete),
|
||||
4 => masked_key(key::End), // VT220/tmux style
|
||||
5 => masked_key(key::PageUp),
|
||||
6 => masked_key(key::PageDown),
|
||||
7 => masked_key(key::Home), // rxvt style
|
||||
8 => masked_key(key::End), // rxvt style
|
||||
11..=15 => masked_key(
|
||||
char::from_u32(u32::from(function_key(1)) + params[0][0] - 11).unwrap(),
|
||||
None,
|
||||
),
|
||||
17..=21 => masked_key(
|
||||
char::from_u32(u32::from(function_key(6)) + params[0][0] - 17).unwrap(),
|
||||
None,
|
||||
),
|
||||
23 | 24 => masked_key(
|
||||
char::from_u32(u32::from(function_key(11)) + params[0][0] - 23).unwrap(),
|
||||
None,
|
||||
),
|
||||
25 | 26 => KeyEvent::from(shift(
|
||||
char::from_u32(u32::from(function_key(3)) + params[0][0] - 25).unwrap(),
|
||||
@@ -1106,7 +1143,7 @@ fn parse_csi(&mut self, buffer: &mut Vec<u8>) -> Option<KeyEvent> {
|
||||
let Some(key) = char::from_u32(params[2][0]) else {
|
||||
return invalid_sequence(buffer);
|
||||
};
|
||||
masked_key(canonicalize_keyed_control_char(key), None)
|
||||
masked_key(canonicalize_keyed_control_char(key))
|
||||
}
|
||||
28 | 29 => KeyEvent::from(shift(
|
||||
char::from_u32(u32::from(function_key(5)) + params[0][0] - 28).unwrap(),
|
||||
@@ -1169,7 +1206,14 @@ fn parse_csi(&mut self, buffer: &mut Vec<u8>) -> Option<KeyEvent> {
|
||||
let Some(shifted_key) = char::from_u32(params[0][1]) else {
|
||||
return invalid_sequence(buffer);
|
||||
};
|
||||
masked_key(key, Some(canonicalize_keyed_control_char(shifted_key)))
|
||||
let Some(base_layout_key) = char::from_u32(params[0][2]) else {
|
||||
return invalid_sequence(buffer);
|
||||
};
|
||||
kitty_key(
|
||||
key,
|
||||
Some(canonicalize_keyed_control_char(shifted_key)),
|
||||
Some(base_layout_key),
|
||||
)
|
||||
}
|
||||
b'Z' => KeyEvent::from(shift(key::Tab)),
|
||||
b'I' => {
|
||||
|
||||
Reference in New Issue
Block a user