Increase support for kitty enhanced keyboard protocol (#688)

This commit is contained in:
Jesse Weaver 2022-07-24 06:06:46 -06:00 committed by GitHub
parent 4dcc6fc058
commit 60e51be726
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 293 additions and 12 deletions

View File

@ -2,7 +2,7 @@
//!
//! cargo run --example event-match-modifiers
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
fn match_event(read_event: Event) {
match read_event {
@ -10,24 +10,29 @@ fn match_event(read_event: Event) {
Event::Key(KeyEvent {
modifiers: KeyModifiers::CONTROL,
code,
..
}) => {
println!("Control + {:?}", code);
}
Event::Key(KeyEvent {
modifiers: KeyModifiers::SHIFT,
code,
..
}) => {
println!("Shift + {:?}", code);
}
Event::Key(KeyEvent {
modifiers: KeyModifiers::ALT,
code,
..
}) => {
println!("Alt + {:?}", code);
}
// Match on multiple modifiers:
Event::Key(KeyEvent { code, modifiers }) => {
Event::Key(KeyEvent {
code, modifiers, ..
}) => {
if modifiers == (KeyModifiers::ALT | KeyModifiers::SHIFT) {
println!("Alt + Shift {:?}", code);
} else {
@ -43,21 +48,26 @@ fn main() {
match_event(Event::Key(KeyEvent {
modifiers: KeyModifiers::CONTROL,
code: KeyCode::Char('z'),
kind: KeyEventKind::Press,
}));
match_event(Event::Key(KeyEvent {
modifiers: KeyModifiers::SHIFT,
code: KeyCode::Left,
kind: KeyEventKind::Press,
}));
match_event(Event::Key(KeyEvent {
modifiers: KeyModifiers::ALT,
code: KeyCode::Delete,
kind: KeyEventKind::Press,
}));
match_event(Event::Key(KeyEvent {
modifiers: KeyModifiers::ALT | KeyModifiers::SHIFT,
code: KeyCode::Right,
kind: KeyEventKind::Press,
}));
match_event(Event::Key(KeyEvent {
modifiers: KeyModifiers::ALT | KeyModifiers::CONTROL,
code: KeyCode::Home,
kind: KeyEventKind::Press,
}));
}

View File

@ -294,6 +294,120 @@ impl Command for DisableMouseCapture {
}
}
bitflags! {
/// Represents special flags that tell compatible terminals to add extra information to keyboard events.
///
/// See <https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement> for more information.
///
/// Alternate keys and Unicode codepoints are not yet supported by crossterm.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct KeyboardEnhancementFlags: u8 {
/// Represent Escape and modified keys using CSI-u sequences, so they can be unambiguously
/// read.
const DISAMBIGUATE_ESCAPE_CODES = 0b0000_0001;
/// Add extra events with [`KeyEvent.kind`] set to [`KeyEventKind::Repeat`] or
/// [`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;
/// 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;
// Send the Unicode codepoint as well as the keycode.
//
// *Note*: this is not yet supported by crossterm.
// const REPORT_ASSOCIATED_TEXT = 0b0001_0000;
}
}
/// A command that enables the [kitty keyboard protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/), which adds extra information to keyboard events and removes ambiguity for modifier keys.
///
/// It should be paired with [`PopKeyboardEnhancementFlags`] at the end of execution.
///
/// Example usage:
/// ```no_run
/// use std::io::{Write, stdout};
/// use crossterm::execute;
/// use crossterm::event::{
/// KeyboardEnhancementFlags,
/// PushKeyboardEnhancementFlags,
/// PopKeyboardEnhancementFlags
/// };
///
/// let mut stdout = stdout();
///
/// execute!(
/// stdout,
/// PushKeyboardEnhancementFlags(
/// KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
/// )
/// );
///
/// // ...
///
/// execute!(stdout, PopKeyboardEnhancementFlags);
/// ```
///
/// Note that, currently, only the following support this protocol:
/// * [kitty terminal](https://sw.kovidgoyal.net/kitty/)
/// * [foot terminal](https://codeberg.org/dnkl/foot/issues/319)
/// * [WezTerm terminal](https://wezfurlong.org/wezterm/config/lua/config/enable_kitty_keyboard.html)
/// * [notcurses library](https://github.com/dankamongmen/notcurses/issues/2131)
/// * [neovim text editor](https://github.com/neovim/neovim/pull/18181)
/// * [kakoune text editor](https://github.com/mawww/kakoune/issues/4103)
/// * [dte text editor](https://gitlab.com/craigbarnes/dte/-/issues/138)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PushKeyboardEnhancementFlags(pub KeyboardEnhancementFlags);
impl Command for PushKeyboardEnhancementFlags {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
write!(f, "{}{}u", csi!(">"), self.0.bits())
}
#[cfg(windows)]
fn execute_winapi(&self) -> Result<()> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"Keyboard progressive enhancement not implemented on Windows.",
))
}
#[cfg(windows)]
fn is_ansi_code_supported(&self) -> bool {
false
}
}
/// A command that disables extra kinds of keyboard events.
///
/// Specifically, it pops one level of keyboard enhancement flags.
///
/// See [`PushKeyboardEnhancementFlags`] and <https://sw.kovidgoyal.net/kitty/keyboard-protocol/> for more information.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PopKeyboardEnhancementFlags;
impl Command for PopKeyboardEnhancementFlags {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
f.write_str(csi!("<1u"))
}
#[cfg(windows)]
fn execute_winapi(&self) -> Result<()> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"Keyboard progressive enhancement not implemented on Windows.",
))
}
#[cfg(windows)]
fn is_ansi_code_supported(&self) -> bool {
false
}
}
/// Represents an event.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
@ -384,6 +498,15 @@ bitflags! {
}
}
/// Represents a keyboard event kind.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
pub enum KeyEventKind {
Press,
Repeat,
Release,
}
/// Represents a key event.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, PartialOrd, Clone, Copy)]
@ -392,11 +515,29 @@ pub struct KeyEvent {
pub code: KeyCode,
/// Additional key modifiers.
pub modifiers: KeyModifiers,
/// Kind of event.
pub kind: KeyEventKind,
}
impl KeyEvent {
pub const fn new(code: KeyCode, modifiers: KeyModifiers) -> KeyEvent {
KeyEvent { code, modifiers }
KeyEvent {
code,
modifiers,
kind: KeyEventKind::Press,
}
}
pub const fn new_with_kind(
code: KeyCode,
modifiers: KeyModifiers,
kind: KeyEventKind,
) -> KeyEvent {
KeyEvent {
code,
modifiers,
kind,
}
}
// modifies the KeyEvent,
@ -422,6 +563,7 @@ impl From<KeyCode> for KeyEvent {
KeyEvent {
code,
modifiers: KeyModifiers::empty(),
kind: KeyEventKind::Press,
}
}
}
@ -431,12 +573,14 @@ impl PartialEq for KeyEvent {
let KeyEvent {
code: lhs_code,
modifiers: lhs_modifiers,
kind: lhs_kind,
} = self.normalize_case();
let KeyEvent {
code: rhs_code,
modifiers: rhs_modifiers,
kind: rhs_kind,
} = other.normalize_case();
(lhs_code == rhs_code) && (lhs_modifiers == rhs_modifiers)
(lhs_code == rhs_code) && (lhs_modifiers == rhs_modifiers) && (lhs_kind == rhs_kind)
}
}
@ -444,9 +588,14 @@ impl Eq for KeyEvent {}
impl Hash for KeyEvent {
fn hash<H: Hasher>(&self, state: &mut H) {
let KeyEvent { code, modifiers } = self.normalize_case();
let KeyEvent {
code,
modifiers,
kind,
} = self.normalize_case();
code.hash(state);
modifiers.hash(state);
kind.hash(state);
}
}

View File

@ -1,7 +1,10 @@
use std::io;
use crate::{
event::{Event, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind},
event::{
Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEvent,
MouseEventKind,
},
ErrorKind, Result,
};
@ -160,6 +163,7 @@ pub(crate) fn parse_csi(buffer: &[u8]) -> Result<Option<InternalEvent>> {
b'Z' => Some(Event::Key(KeyEvent {
code: KeyCode::BackTab,
modifiers: KeyModifiers::SHIFT,
kind: KeyEventKind::Press,
})),
b'M' => return parse_csi_normal_mouse(buffer),
b'<' => return parse_csi_sgr_mouse(buffer),
@ -200,6 +204,21 @@ where
.map_err(|_| could_not_parse_event_error())
}
fn modifier_and_kind_parsed(iter: &mut dyn Iterator<Item = &str>) -> Result<(u8, u8)> {
let mut sub_split = iter
.next()
.ok_or_else(could_not_parse_event_error)?
.split(':');
let modifier_mask = next_parsed::<u8>(&mut sub_split)?;
if let Ok(kind_code) = next_parsed::<u8>(&mut sub_split) {
Ok((modifier_mask, kind_code))
} else {
Ok((modifier_mask, 1))
}
}
pub(crate) fn parse_csi_cursor_position(buffer: &[u8]) -> Result<Option<InternalEvent>> {
// ESC [ Cy ; Cx R
// Cy - cursor row number (starting from 1)
@ -233,6 +252,15 @@ fn parse_modifiers(mask: u8) -> KeyModifiers {
modifiers
}
fn parse_key_event_kind(kind: u8) -> KeyEventKind {
match kind {
1 => KeyEventKind::Press,
2 => KeyEventKind::Repeat,
3 => KeyEventKind::Release,
_ => KeyEventKind::Press,
}
}
pub(crate) fn parse_csi_modifier_key_code(buffer: &[u8]) -> Result<Option<InternalEvent>> {
assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [
@ -273,11 +301,15 @@ pub(crate) fn parse_csi_u_encoded_key_code(buffer: &[u8]) -> Result<Option<Inter
// codepoint: ASCII Dec value
let codepoint = next_parsed::<u32>(&mut split)?;
let modifiers = if let Ok(modifier_mask) = next_parsed::<u8>(&mut split) {
parse_modifiers(modifier_mask)
} else {
KeyModifiers::NONE
};
let (modifiers, kind) =
if let Ok((modifier_mask, kind_code)) = modifier_and_kind_parsed(&mut split) {
(
parse_modifiers(modifier_mask),
parse_key_event_kind(kind_code),
)
} else {
(KeyModifiers::NONE, KeyEventKind::Press)
};
let keycode = {
if let Some(c) = char::from_u32(codepoint) {
@ -304,7 +336,7 @@ pub(crate) fn parse_csi_u_encoded_key_code(buffer: &[u8]) -> Result<Option<Inter
}
};
let input_event = Event::Key(KeyEvent::new(keycode, modifiers));
let input_event = Event::Key(KeyEvent::new_with_kind(keycode, modifiers, kind));
Ok(Some(InternalEvent::Event(input_event)))
}
@ -844,4 +876,91 @@ mod tests {
)))),
);
}
#[test]
fn test_parse_basic_csi_u_encoded_key_code() {
assert_eq!(
parse_csi_u_encoded_key_code(b"\x1B[97u").unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
KeyCode::Char('a'),
KeyModifiers::empty()
)))),
);
assert_eq!(
parse_csi_u_encoded_key_code(b"\x1B[97;2u").unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
KeyCode::Char('A'),
KeyModifiers::SHIFT
)))),
);
assert_eq!(
parse_csi_u_encoded_key_code(b"\x1B[97;7u").unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
KeyCode::Char('a'),
KeyModifiers::ALT | KeyModifiers::CONTROL
)))),
);
}
#[test]
fn test_parse_basic_csi_u_encoded_key_code_special_keys() {
assert_eq!(
parse_csi_u_encoded_key_code(b"\x1B[13u").unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
KeyCode::Enter,
KeyModifiers::empty()
)))),
);
assert_eq!(
parse_csi_u_encoded_key_code(b"\x1B[27u").unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
KeyCode::Esc,
KeyModifiers::empty()
)))),
);
}
#[test]
fn test_parse_csi_u_encoded_key_code_with_types() {
assert_eq!(
parse_csi_u_encoded_key_code(b"\x1B[97;1u").unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind(
KeyCode::Char('a'),
KeyModifiers::empty(),
KeyEventKind::Press,
)))),
);
assert_eq!(
parse_csi_u_encoded_key_code(b"\x1B[97;1:1u").unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind(
KeyCode::Char('a'),
KeyModifiers::empty(),
KeyEventKind::Press,
)))),
);
assert_eq!(
parse_csi_u_encoded_key_code(b"\x1B[97;5:1u").unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind(
KeyCode::Char('a'),
KeyModifiers::CONTROL,
KeyEventKind::Press,
)))),
);
assert_eq!(
parse_csi_u_encoded_key_code(b"\x1B[97;1:2u").unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind(
KeyCode::Char('a'),
KeyModifiers::empty(),
KeyEventKind::Repeat,
)))),
);
assert_eq!(
parse_csi_u_encoded_key_code(b"\x1B[97;1:3u").unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind(
KeyCode::Char('a'),
KeyModifiers::empty(),
KeyEventKind::Release,
)))),
);
}
}

View File

@ -51,6 +51,9 @@
//! - Shape -
//! [`SetCursorShape`](cursor/struct.SetCursorShape.html)
//! - Module [`event`](event/index.html)
//! - Keyboard events -
//! [`PushKeyboardEnhancementFlags`](event/struct.PushKeyboardEnhancementFlags.html),
//! [`PopKeyboardEnhancementFlags`](event/struct.PopKeyboardEnhancementFlags.html)
//! - Mouse events - [`EnableMouseCapture`](event/struct.EnableMouseCapture.html),
//! [`DisableMouseCapture`](event/struct.DisableMouseCapture.html)
//! - Module [`style`](style/index.html)