Implement "report alternate keys" from the Kitty Keyboard Protocol (#754)
The "report alternate keys" part of the Kitty keyboard protocol will send an additional codepoint containing the "shifted" version of a key based on the keyboard layout. This is useful for downstream applications which set up keybindings based on symbols instead of exact keys being pressed. For example, underscore (_) with the Alt modifier is sent as minus (-) with Alt and Shift modifiers. A terminal will send the underscore codepoint as an alternate though, and we can use that information and the presence of the Shift modifier to resolve the symbol. Other examples are 'A-(' (sent as 'A-S-9') and 'A-)' (sent as 'A-S-0'). This change allows pushing the "report alternate keys" flag and overwrites the keycode and modifiers for any shifted keys sent by the terminal.
This commit is contained in:
parent
383d9a7827
commit
bca71adad7
@ -81,6 +81,7 @@ fn main() -> Result<()> {
|
|||||||
PushKeyboardEnhancementFlags(
|
PushKeyboardEnhancementFlags(
|
||||||
KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
|
KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
|
||||||
| KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES
|
| KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES
|
||||||
|
| KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS
|
||||||
| KeyboardEnhancementFlags::REPORT_EVENT_TYPES
|
| KeyboardEnhancementFlags::REPORT_EVENT_TYPES
|
||||||
)
|
)
|
||||||
)?;
|
)?;
|
||||||
|
@ -319,10 +319,9 @@ bitflags! {
|
|||||||
/// [`KeyEventKind::Release`] when keys are autorepeated or released.
|
/// [`KeyEventKind::Release`] when keys are autorepeated or released.
|
||||||
const REPORT_EVENT_TYPES = 0b0000_0010;
|
const REPORT_EVENT_TYPES = 0b0000_0010;
|
||||||
// Send [alternate keycodes](https://sw.kovidgoyal.net/kitty/keyboard-protocol/#key-codes)
|
// Send [alternate keycodes](https://sw.kovidgoyal.net/kitty/keyboard-protocol/#key-codes)
|
||||||
// in addition to the base keycode.
|
// in addition to the base keycode. The alternate keycode overrides the base keycode in
|
||||||
//
|
// resulting `KeyEvent`s.
|
||||||
// *Note*: these are not yet supported by crossterm.
|
const REPORT_ALTERNATE_KEYS = 0b0000_0100;
|
||||||
// const REPORT_ALTERNATE_KEYS = 0b0000_0100;
|
|
||||||
/// Represent all keyboard events as CSI-u sequences. This is required to get repeat/release
|
/// Represent all keyboard events as CSI-u sequences. This is required to get repeat/release
|
||||||
/// events for plain-text keys.
|
/// events for plain-text keys.
|
||||||
const REPORT_ALL_KEYS_AS_ESCAPE_CODES = 0b0000_1000;
|
const REPORT_ALL_KEYS_AS_ESCAPE_CODES = 0b0000_1000;
|
||||||
|
@ -275,10 +275,9 @@ fn parse_csi_keyboard_enhancement_flags(buffer: &[u8]) -> Result<Option<Internal
|
|||||||
if bits & 2 != 0 {
|
if bits & 2 != 0 {
|
||||||
flags |= KeyboardEnhancementFlags::REPORT_EVENT_TYPES;
|
flags |= KeyboardEnhancementFlags::REPORT_EVENT_TYPES;
|
||||||
}
|
}
|
||||||
// *Note*: this is not yet supported by crossterm.
|
if bits & 4 != 0 {
|
||||||
// if bits & 4 != 0 {
|
flags |= KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS;
|
||||||
// flags |= KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS;
|
}
|
||||||
// }
|
|
||||||
if bits & 8 != 0 {
|
if bits & 8 != 0 {
|
||||||
flags |= KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES;
|
flags |= KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES;
|
||||||
}
|
}
|
||||||
@ -500,14 +499,33 @@ pub(crate) fn parse_csi_u_encoded_key_code(buffer: &[u8]) -> Result<Option<Inter
|
|||||||
assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [
|
assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [
|
||||||
assert!(buffer.ends_with(&[b'u']));
|
assert!(buffer.ends_with(&[b'u']));
|
||||||
|
|
||||||
|
// This function parses `CSI … u` sequences. These are sequences defined in either
|
||||||
|
// the `CSI u` (a.k.a. "Fix Keyboard Input on Terminals - Please", https://www.leonerd.org.uk/hacks/fixterms/)
|
||||||
|
// or Kitty Keyboard Protocol (https://sw.kovidgoyal.net/kitty/keyboard-protocol/) specifications.
|
||||||
|
// This CSI sequence is a tuple of semicolon-separated numbers.
|
||||||
let s = std::str::from_utf8(&buffer[2..buffer.len() - 1])
|
let s = std::str::from_utf8(&buffer[2..buffer.len() - 1])
|
||||||
.map_err(|_| could_not_parse_event_error())?;
|
.map_err(|_| could_not_parse_event_error())?;
|
||||||
let mut split = s.split(';');
|
let mut split = s.split(';');
|
||||||
|
|
||||||
// This CSI sequence a tuple of semicolon-separated numbers.
|
// In `CSI u`, this is parsed as:
|
||||||
// CSI [codepoint];[modifiers] u
|
//
|
||||||
// codepoint: ASCII Dec value
|
// CSI codepoint ; modifiers u
|
||||||
let codepoint = next_parsed::<u32>(&mut split)?;
|
// codepoint: ASCII Dec value
|
||||||
|
//
|
||||||
|
// The Kitty Keyboard Protocol extends this with optional components that can be
|
||||||
|
// enabled progressively. The full sequence is parsed as:
|
||||||
|
//
|
||||||
|
// CSI unicode-key-code:alternate-key-codes ; modifiers:event-type ; text-as-codepoints u
|
||||||
|
let mut codepoints = split
|
||||||
|
.next()
|
||||||
|
.ok_or_else(could_not_parse_event_error)?
|
||||||
|
.split(':');
|
||||||
|
|
||||||
|
let codepoint = codepoints
|
||||||
|
.next()
|
||||||
|
.ok_or_else(could_not_parse_event_error)?
|
||||||
|
.parse::<u32>()
|
||||||
|
.map_err(|_| could_not_parse_event_error())?;
|
||||||
|
|
||||||
let (mut modifiers, kind, state_from_modifiers) =
|
let (mut modifiers, kind, state_from_modifiers) =
|
||||||
if let Ok((modifier_mask, kind_code)) = modifier_and_kind_parsed(&mut split) {
|
if let Ok((modifier_mask, kind_code)) = modifier_and_kind_parsed(&mut split) {
|
||||||
@ -520,7 +538,7 @@ pub(crate) fn parse_csi_u_encoded_key_code(buffer: &[u8]) -> Result<Option<Inter
|
|||||||
(KeyModifiers::NONE, KeyEventKind::Press, KeyEventState::NONE)
|
(KeyModifiers::NONE, KeyEventKind::Press, KeyEventState::NONE)
|
||||||
};
|
};
|
||||||
|
|
||||||
let (keycode, state_from_keycode) = {
|
let (mut keycode, state_from_keycode) = {
|
||||||
if let Some((special_key_code, state)) = translate_functional_key_code(codepoint) {
|
if let Some((special_key_code, state)) = translate_functional_key_code(codepoint) {
|
||||||
(special_key_code, state)
|
(special_key_code, state)
|
||||||
} else if let Some(c) = char::from_u32(codepoint) {
|
} else if let Some(c) = char::from_u32(codepoint) {
|
||||||
@ -574,6 +592,21 @@ pub(crate) fn parse_csi_u_encoded_key_code(buffer: &[u8]) -> Result<Option<Inter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When the "report alternate keys" flag is enabled in the Kitty Keyboard Protocol
|
||||||
|
// and the terminal sends a keyboard event containing shift, the sequence will
|
||||||
|
// contain an additional codepoint separated by a ':' character which contains
|
||||||
|
// the shifted character according to the keyboard layout.
|
||||||
|
if modifiers.contains(KeyModifiers::SHIFT) {
|
||||||
|
if let Some(shifted_c) = codepoints
|
||||||
|
.next()
|
||||||
|
.and_then(|codepoint| codepoint.parse::<u32>().ok())
|
||||||
|
.and_then(char::from_u32)
|
||||||
|
{
|
||||||
|
keycode = KeyCode::Char(shifted_c);
|
||||||
|
modifiers.set(KeyModifiers::SHIFT, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let input_event = Event::Key(KeyEvent::new_with_kind_and_state(
|
let input_event = Event::Key(KeyEvent::new_with_kind_and_state(
|
||||||
keycode,
|
keycode,
|
||||||
modifiers,
|
modifiers,
|
||||||
@ -1410,6 +1443,26 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_csi_u_with_shifted_keycode() {
|
||||||
|
assert_eq!(
|
||||||
|
// A-S-9 is equivalent to A-(
|
||||||
|
parse_event(b"\x1B[57:40;4u", false).unwrap(),
|
||||||
|
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||||
|
KeyCode::Char('('),
|
||||||
|
KeyModifiers::ALT,
|
||||||
|
)))),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
// A-S-minus is equivalent to A-_
|
||||||
|
parse_event(b"\x1B[45:95;4u", false).unwrap(),
|
||||||
|
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||||
|
KeyCode::Char('_'),
|
||||||
|
KeyModifiers::ALT,
|
||||||
|
)))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_csi_special_key_code_with_types() {
|
fn test_parse_csi_special_key_code_with_types() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
Loading…
Reference in New Issue
Block a user