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:
Michael Davis 2023-02-11 03:40:11 -06:00 committed by GitHub
parent 383d9a7827
commit bca71adad7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 66 additions and 13 deletions

View File

@ -81,6 +81,7 @@ fn main() -> Result<()> {
PushKeyboardEnhancementFlags(
KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
| KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES
| KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS
| KeyboardEnhancementFlags::REPORT_EVENT_TYPES
)
)?;

View File

@ -319,10 +319,9 @@ bitflags! {
/// [`KeyEventKind::Release`] when keys are autorepeated or released.
const REPORT_EVENT_TYPES = 0b0000_0010;
// Send [alternate keycodes](https://sw.kovidgoyal.net/kitty/keyboard-protocol/#key-codes)
// in addition to the base keycode.
//
// *Note*: these are not yet supported by crossterm.
// const REPORT_ALTERNATE_KEYS = 0b0000_0100;
// in addition to the base keycode. The alternate keycode overrides the base keycode in
// resulting `KeyEvent`s.
const REPORT_ALTERNATE_KEYS = 0b0000_0100;
/// Represent all keyboard events as CSI-u sequences. This is required to get repeat/release
/// events for plain-text keys.
const REPORT_ALL_KEYS_AS_ESCAPE_CODES = 0b0000_1000;

View File

@ -275,10 +275,9 @@ fn parse_csi_keyboard_enhancement_flags(buffer: &[u8]) -> Result<Option<Internal
if bits & 2 != 0 {
flags |= KeyboardEnhancementFlags::REPORT_EVENT_TYPES;
}
// *Note*: this is not yet supported by crossterm.
// if bits & 4 != 0 {
// flags |= KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS;
// }
if bits & 4 != 0 {
flags |= KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS;
}
if bits & 8 != 0 {
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.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])
.map_err(|_| could_not_parse_event_error())?;
let mut split = s.split(';');
// This CSI sequence a tuple of semicolon-separated numbers.
// CSI [codepoint];[modifiers] u
// codepoint: ASCII Dec value
let codepoint = next_parsed::<u32>(&mut split)?;
// In `CSI u`, this is parsed as:
//
// CSI codepoint ; modifiers u
// 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) =
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)
};
let (keycode, state_from_keycode) = {
let (mut keycode, state_from_keycode) = {
if let Some((special_key_code, state)) = translate_functional_key_code(codepoint) {
(special_key_code, state)
} 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(
keycode,
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]
fn test_parse_csi_special_key_code_with_types() {
assert_eq!(