Increase support for kitty enhanced keyboard protocol (#688)
This commit is contained in:
parent
4dcc6fc058
commit
60e51be726
@ -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,
|
||||
}));
|
||||
}
|
||||
|
155
src/event.rs
155
src/event.rs
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
)))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user