diff --git a/examples/event-match-modifiers.rs b/examples/event-match-modifiers.rs index 9ebd19f2a..9f82998c6 100644 --- a/examples/event-match-modifiers.rs +++ b/examples/event-match-modifiers.rs @@ -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, })); } diff --git a/src/event.rs b/src/event.rs index c7d372d37..d3152b45a 100644 --- a/src/event.rs +++ b/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 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 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 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(&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); } } diff --git a/src/event/sys/unix/parse.rs b/src/event/sys/unix/parse.rs index 37bf54c3b..4e127cac8 100644 --- a/src/event/sys/unix/parse.rs +++ b/src/event/sys/unix/parse.rs @@ -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> { 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) -> Result<(u8, u8)> { + let mut sub_split = iter + .next() + .ok_or_else(could_not_parse_event_error)? + .split(':'); + + let modifier_mask = next_parsed::(&mut sub_split)?; + + if let Ok(kind_code) = next_parsed::(&mut sub_split) { + Ok((modifier_mask, kind_code)) + } else { + Ok((modifier_mask, 1)) + } +} + pub(crate) fn parse_csi_cursor_position(buffer: &[u8]) -> Result> { // 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> { assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [ @@ -273,11 +301,15 @@ pub(crate) fn parse_csi_u_encoded_key_code(buffer: &[u8]) -> Result(&mut split)?; - let modifiers = if let Ok(modifier_mask) = next_parsed::(&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