Skip to content

Commit

Permalink
Implement "report alternate keys" from the Kitty Keyboard Protocol (#754
Browse files Browse the repository at this point in the history
)

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.
  • Loading branch information
the-mikedavis authored Feb 11, 2023
1 parent 383d9a7 commit bca71ad
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 13 deletions.
1 change: 1 addition & 0 deletions examples/event-read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
)?;
Expand Down
7 changes: 3 additions & 4 deletions src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
71 changes: 62 additions & 9 deletions src/event/sys/unix/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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!(
Expand Down

0 comments on commit bca71ad

Please sign in to comment.