Add is_* and as_* methods to the event enums (#949)

* Add is_ and as_ methods to the event enums

Often application code only cares about a small subset of possible
events. These methods make it simpler to write code which checks whether
an event is a particular event type or converts events into the specific
type (returning an Option).

This can help simplify some nested match blocks. E.g.:

```rust
match event {
    Event::Key(key) if key.kind == KeyEventKind::Press => { ... }
}
```

becomes:

```rust
if let Some(key) = event.as_key_press() { ... }
```

Similar flexible methods are aded across all the event enums:

- `Event::is_focus_gained()`
- `Event::is_focus_lost()`
- `Event::is_key()`
- `Event::is_mouse()`
- `Event::is_paste()`
- `Event::is_resize()`
- `Event::is_key_press()`
- `Event::as_key_press() -> Option<&KeyEvent>`
- `MouseEventKind::is_*()`
- `MouseButton::is_*()`
- `KeyEventKind::is_*()`
- `KeyEvent::is_press()`
- `KeyEvent::is_release()`
- `KeyEvent::is_repeat()`
- `KeyCode::is_*()`
- `KeyCode::is_function_key(n)`
- `KeyCode::is_char(c)`
- `KeyCode::as_char() -> Option<char>`
- `KeyCode::is_media_key(media)`
- `KeyCode::is_modifier(modifier)`
- add is_key_release() and is_key_repeat() checks
- add as_key_event()
- rename as_key_press() to as_key_press_event()
- add as_key_repeat_event()
- add as_key_release_event()
- add as_mouse_event()
- add as_paste_event()
- more tests
- update event-match and key-display examples
This commit is contained in:
Josh McKinney 2025-02-02 02:31:10 -08:00 committed by GitHub
parent 1bcfa9729c
commit e063091312
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 450 additions and 55 deletions

View File

@ -46,6 +46,7 @@ use-dev-tty = ["filedescriptor", "rustix/process"]
[dependencies]
bitflags = { version = "2.3" }
derive_more = { version = "1.0.0", features = ["is_variant"] }
document-features = "0.2.10"
futures-core = { version = "0.3", optional = true, default-features = false }
parking_lot = "0.12"

View File

@ -4,43 +4,42 @@
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
fn match_event(read_event: Event) {
match read_event {
// Match one one modifier:
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);
}
fn match_event(event: Event) {
if let Some(key) = event.as_key_press_event() {
match key {
KeyEvent {
modifiers: KeyModifiers::CONTROL,
code,
..
} => {
println!("Control + {:?}", code);
}
KeyEvent {
modifiers: KeyModifiers::SHIFT,
code,
..
} => {
println!("Shift + {:?}", code);
}
KeyEvent {
modifiers: KeyModifiers::ALT,
code,
..
} => {
println!("Alt + {:?}", code);
}
// Match on multiple modifiers:
Event::Key(KeyEvent {
code, modifiers, ..
}) => {
if modifiers == (KeyModifiers::ALT | KeyModifiers::SHIFT) {
println!("Alt + Shift {:?}", code);
} else {
println!("({:?}) with key: {:?}", modifiers, code)
// Match on multiple modifiers:
KeyEvent {
code, modifiers, ..
} => {
if modifiers == (KeyModifiers::ALT | KeyModifiers::SHIFT) {
println!("Alt + Shift {:?}", code);
} else {
println!("({:?}) with key: {:?}", modifiers, code)
}
}
}
_ => {}
}
}

View File

@ -7,9 +7,9 @@
use std::io;
use crossterm::event::{KeyEventKind, KeyModifiers};
use crossterm::event::KeyModifiers;
use crossterm::{
event::{read, Event, KeyCode},
event::{read, KeyCode},
terminal::{disable_raw_mode, enable_raw_mode},
};
@ -29,20 +29,17 @@ fn main() -> io::Result<()> {
}
fn print_events() -> io::Result<()> {
loop {
let event = read()?;
match event {
Event::Key(event) if event.kind == KeyEventKind::Press => {
print!("Key pressed: ");
if event.modifiers != KeyModifiers::NONE {
print!("{}+", event.modifiers);
}
println!("{}\r", event.code);
if event.code == KeyCode::Esc {
break;
}
}
_ => {}
while let Ok(event) = read() {
let Some(event) = event.as_key_press_event() else {
continue;
};
let modifier = match event.modifiers {
KeyModifiers::NONE => "".to_string(),
_ => format!("{:}+", event.modifiers),
};
println!("Key pressed: {modifier}{code}\r", code = event.code);
if event.code == KeyCode::Esc {
break;
}
}
Ok(())

View File

@ -126,6 +126,7 @@ pub(crate) mod stream;
pub(crate) mod sys;
pub(crate) mod timeout;
use derive_more::derive::IsVariant;
#[cfg(feature = "event-stream")]
pub use stream::EventStream;
@ -543,7 +544,7 @@ impl Command for PopKeyboardEnhancementFlags {
/// Represents an event.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(not(feature = "bracketed-paste"), derive(Copy))]
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Hash)]
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Hash, IsVariant)]
pub enum Event {
/// The terminal gained focus
FocusGained,
@ -562,6 +563,198 @@ pub enum Event {
Resize(u16, u16),
}
impl Event {
/// Returns `true` if the event is a key press event.
///
/// This is useful for waiting for any key press event, regardless of the key that was pressed.
///
/// Returns `false` for key release and repeat events (as well as for non-key events).
///
/// # Examples
///
/// The following code runs a loop that processes events until a key press event is encountered:
///
/// ```no_run
/// use crossterm::event;
///
/// while !event::read()?.is_key_press() {
/// // ...
/// }
/// # Ok::<(), std::io::Error>(())
/// ```
#[inline]
pub fn is_key_press(&self) -> bool {
matches!(
self,
Event::Key(KeyEvent {
kind: KeyEventKind::Press,
..
})
)
}
/// Returns `true` if the event is a key release event.
#[inline]
pub fn is_key_release(&self) -> bool {
matches!(
self,
Event::Key(KeyEvent {
kind: KeyEventKind::Release,
..
})
)
}
/// Returns `true` if the event is a key repeat event.
#[inline]
pub fn is_key_repeat(&self) -> bool {
matches!(
self,
Event::Key(KeyEvent {
kind: KeyEventKind::Repeat,
..
})
)
}
/// Returns the key event if the event is a key event, otherwise `None`.
///
/// This is a convenience method that makes apps that only care about key events easier to write.
///
/// # Examples
///
/// The following code runs a loop that only processes key events:
///
/// ```no_run
/// use crossterm::event;
///
/// while let Some(key_event) = event::read()?.as_key_event() {
/// // ...
/// }
/// # std::io::Result::Ok(())
/// ```
#[inline]
pub fn as_key_event(&self) -> Option<KeyEvent> {
match self {
Event::Key(event) => Some(*event),
_ => None,
}
}
/// Returns an Option containing the KeyEvent if the event is a key press event.
///
/// This is a convenience method that makes apps that only care about key press events, and not
/// key release or repeat events (or non-key events), easier to write.
///
/// Returns `None` for key release and repeat events (as well as for non-key events).
///
/// # Examples
///
/// The following code runs a loop that only processes key press events:
///
/// ```no_run
/// use crossterm::event;
///
/// while let Ok(event) = event::read() {
/// if let Some(key) = event.as_key_press_event() {
/// // ...
/// }
/// }
#[inline]
pub fn as_key_press_event(&self) -> Option<KeyEvent> {
match self {
Event::Key(event) if self.is_key_press() => Some(*event),
_ => None,
}
}
/// Returns an Option containing the `KeyEvent` if the event is a key release event.
#[inline]
pub fn as_key_release_event(&self) -> Option<KeyEvent> {
match self {
Event::Key(event) if event.kind == KeyEventKind::Release => Some(*event),
_ => None,
}
}
/// Returns an Option containing the `KeyEvent` if the event is a key repeat event.
#[inline]
pub fn as_key_repeat_event(&self) -> Option<KeyEvent> {
match self {
Event::Key(event) if event.kind == KeyEventKind::Repeat => Some(*event),
_ => None,
}
}
/// Returns the mouse event if the event is a mouse event, otherwise `None`.
///
/// This is a convenience method that makes code which only cares about mouse events easier to
/// write.
///
/// # Examples
///
/// ```no_run
/// use crossterm::event;
///
/// while let Some(mouse_event) = event::read()?.as_mouse_event() {
/// // ...
/// }
/// # std::io::Result::Ok(())
/// ```
#[inline]
pub fn as_mouse_event(&self) -> Option<MouseEvent> {
match self {
Event::Mouse(event) => Some(*event),
_ => None,
}
}
/// Returns the pasted string if the event is a paste event, otherwise `None`.
///
/// This is a convenience method that makes code which only cares about paste events easier to write.
///
/// # Examples
///
/// ```no_run
/// use crossterm::event;
///
/// while let Some(paste) = event::read()?.as_paste_event() {
/// // ...
/// }
/// # std::io::Result::Ok(())
/// ```
#[cfg(feature = "bracketed-paste")]
#[inline]
pub fn as_paste_event(&self) -> Option<&str> {
match self {
Event::Paste(paste) => Some(paste),
_ => None,
}
}
/// Returns the size as a tuple if the event is a resize event, otherwise `None`.
///
/// This is a convenience method that makes code which only cares about resize events easier to write.
///
/// # Examples
///
/// ```no_run
/// use crossterm::event;
///
/// while let Some((columns, rows)) = event::read()?.as_resize_event() {
/// // ...
/// }
/// # std::io::Result::Ok(())
/// ```
#[inline]
pub fn as_resize_event(&self) -> Option<(u16, u16)> {
match self {
Event::Resize(columns, rows) => Some((*columns, *rows)),
_ => None,
}
}
}
/// Represents a mouse event.
///
/// # Platform-specific Notes
@ -600,7 +793,7 @@ pub struct MouseEvent {
/// `MouseEventKind::Up` and `MouseEventKind::Drag` events. `MouseButton::Left`
/// is returned if we don't know which button was used.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash, IsVariant)]
pub enum MouseEventKind {
/// Pressed mouse button. Contains the button that was pressed.
Down(MouseButton),
@ -622,7 +815,7 @@ pub enum MouseEventKind {
/// Represents a mouse button.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash, IsVariant)]
pub enum MouseButton {
/// Left mouse button.
Left,
@ -702,7 +895,7 @@ impl Display for KeyModifiers {
/// Represents a keyboard event kind.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash, IsVariant)]
pub enum KeyEventKind {
Press,
Repeat,
@ -806,6 +999,21 @@ impl KeyEvent {
}
self
}
/// Returns whether the key event is a press event.
pub fn is_press(&self) -> bool {
self.kind.is_press()
}
/// Returns whether the key event is a release event.
pub fn is_release(&self) -> bool {
self.kind.is_release()
}
/// Returns whether the key event is a repeat event.
pub fn is_repeat(&self) -> bool {
self.kind.is_repeat()
}
}
impl From<KeyCode> for KeyEvent {
@ -1006,7 +1214,7 @@ impl Display for ModifierKeyCode {
}
/// Represents a key.
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash, IsVariant)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum KeyCode {
/// Backspace key (Delete on macOS, Backspace on other platforms).
@ -1040,10 +1248,12 @@ pub enum KeyCode {
/// F key.
///
/// `KeyCode::F(1)` represents F1 key, etc.
#[is_variant(ignore)]
F(u8),
/// A character.
///
/// `KeyCode::Char('c')` represents `c` character, etc.
#[is_variant(ignore)]
Char(char),
/// Null.
Null,
@ -1096,6 +1306,7 @@ pub enum KeyCode {
/// **Note:** these keys can only be read if
/// [`KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES`] has been enabled with
/// [`PushKeyboardEnhancementFlags`].
#[is_variant(ignore)]
Media(MediaKeyCode),
/// A modifier key.
///
@ -1103,9 +1314,92 @@ pub enum KeyCode {
/// [`KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES`] and
/// [`KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES`] have been enabled with
/// [`PushKeyboardEnhancementFlags`].
#[is_variant(ignore)]
Modifier(ModifierKeyCode),
}
impl KeyCode {
/// Returns `true` if the key code is the given function key.
///
/// # Examples
///
/// ```
/// # use crossterm::event::KeyCode;
/// assert!(KeyCode::F(1).is_function_key(1));
/// assert!(!KeyCode::F(1).is_function_key(2));
/// ```
pub fn is_function_key(&self, n: u8) -> bool {
matches!(self, KeyCode::F(m) if *m == n)
}
/// Returns `true` if the key code is the given character.
///
/// # Examples
///
/// ```
/// # use crossterm::event::KeyCode;
/// assert!(KeyCode::Char('a').is_char('a'));
/// assert!(!KeyCode::Char('a').is_char('b'));
/// assert!(!KeyCode::F(1).is_char('a'));
/// ```
pub fn is_char(&self, c: char) -> bool {
matches!(self, KeyCode::Char(m) if *m == c)
}
/// Returns the character if the key code is a character key.
///
/// Returns `None` if the key code is not a character key.
///
/// # Examples
///
/// ```
/// # use crossterm::event::KeyCode;
/// assert_eq!(KeyCode::Char('a').as_char(), Some('a'));
/// assert_eq!(KeyCode::F(1).as_char(), None);
/// ```
pub fn as_char(&self) -> Option<char> {
match self {
KeyCode::Char(c) => Some(*c),
_ => None,
}
}
/// Returns `true` if the key code is the given media key.
///
/// **Note:** this method requires
/// [`KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES`] to be enabled with
/// [`PushKeyboardEnhancementFlags`].
///
/// # Examples
///
/// ```
/// # use crossterm::event::{KeyCode, MediaKeyCode};
/// assert!(KeyCode::Media(MediaKeyCode::Play).is_media_key(MediaKeyCode::Play));
/// assert!(!KeyCode::Media(MediaKeyCode::Play).is_media_key(MediaKeyCode::Pause));
/// ```
pub fn is_media_key(&self, media: MediaKeyCode) -> bool {
matches!(self, KeyCode::Media(m) if *m == media)
}
/// Returns `true` if the key code is the given modifier key.
///
/// **Note:** this method requires both
/// [`KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES`] and
/// [`KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES`] to be enabled with
/// [`PushKeyboardEnhancementFlags`].
///
/// # Examples
///
/// ```
/// # use crossterm::event::{KeyCode, ModifierKeyCode};
/// assert!(KeyCode::Modifier(ModifierKeyCode::LeftShift).is_modifier(ModifierKeyCode::LeftShift));
/// assert!(!KeyCode::Modifier(ModifierKeyCode::LeftShift).is_modifier(ModifierKeyCode::RightShift));
/// ```
pub fn is_modifier(&self, modifier: ModifierKeyCode) -> bool {
matches!(self, KeyCode::Modifier(m) if *m == modifier)
}
}
impl Display for KeyCode {
/// Formats the `KeyCode` using the given formatter.
///
@ -1324,4 +1618,108 @@ mod tests {
assert_eq!(format!("{}", Modifier(RightAlt)), "Right Alt");
assert_eq!(format!("{}", Modifier(RightSuper)), "Right Super");
}
const ESC_PRESSED: KeyEvent =
KeyEvent::new_with_kind(KeyCode::Esc, KeyModifiers::empty(), KeyEventKind::Press);
const ESC_RELEASED: KeyEvent =
KeyEvent::new_with_kind(KeyCode::Esc, KeyModifiers::empty(), KeyEventKind::Release);
const ESC_REPEAT: KeyEvent =
KeyEvent::new_with_kind(KeyCode::Esc, KeyModifiers::empty(), KeyEventKind::Repeat);
const MOUSE_CLICK: MouseEvent = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 1,
row: 1,
modifiers: KeyModifiers::empty(),
};
#[test]
fn event_is() {
let event = Event::FocusGained;
assert!(event.is_focus_gained());
assert!(event.is_focus_gained());
assert!(!event.is_key());
let event = Event::FocusLost;
assert!(event.is_focus_lost());
assert!(!event.is_focus_gained());
assert!(!event.is_key());
let event = Event::Resize(1, 1);
assert!(event.is_resize());
assert!(!event.is_key());
let event = Event::Key(ESC_PRESSED);
assert!(event.is_key());
assert!(event.is_key_press());
assert!(!event.is_key_release());
assert!(!event.is_key_repeat());
assert!(!event.is_focus_gained());
let event = Event::Key(ESC_RELEASED);
assert!(event.is_key());
assert!(!event.is_key_press());
assert!(event.is_key_release());
assert!(!event.is_key_repeat());
assert!(!event.is_focus_gained());
let event = Event::Key(ESC_REPEAT);
assert!(event.is_key());
assert!(!event.is_key_press());
assert!(!event.is_key_release());
assert!(event.is_key_repeat());
assert!(!event.is_focus_gained());
let event = Event::Mouse(MOUSE_CLICK);
assert!(event.is_mouse());
assert!(!event.is_key());
#[cfg(feature = "bracketed-paste")]
{
let event = Event::Paste("".to_string());
assert!(event.is_paste());
assert!(!event.is_key());
}
}
#[test]
fn event_as() {
let event = Event::FocusGained;
assert_eq!(event.as_key_event(), None);
let event = Event::Key(ESC_PRESSED);
assert_eq!(event.as_key_event(), Some(ESC_PRESSED));
assert_eq!(event.as_key_press_event(), Some(ESC_PRESSED));
assert_eq!(event.as_key_release_event(), None);
assert_eq!(event.as_key_repeat_event(), None);
assert_eq!(event.as_resize_event(), None);
let event = Event::Key(ESC_RELEASED);
assert_eq!(event.as_key_event(), Some(ESC_RELEASED));
assert_eq!(event.as_key_release_event(), Some(ESC_RELEASED));
assert_eq!(event.as_key_press_event(), None);
assert_eq!(event.as_key_repeat_event(), None);
assert_eq!(event.as_resize_event(), None);
let event = Event::Key(ESC_REPEAT);
assert_eq!(event.as_key_event(), Some(ESC_REPEAT));
assert_eq!(event.as_key_repeat_event(), Some(ESC_REPEAT));
assert_eq!(event.as_key_press_event(), None);
assert_eq!(event.as_key_release_event(), None);
assert_eq!(event.as_resize_event(), None);
let event = Event::Resize(1, 1);
assert_eq!(event.as_resize_event(), Some((1, 1)));
assert_eq!(event.as_key_event(), None);
let event = Event::Mouse(MOUSE_CLICK);
assert_eq!(event.as_mouse_event(), Some(MOUSE_CLICK));
assert_eq!(event.as_key_event(), None);
#[cfg(feature = "bracketed-paste")]
{
let event = Event::Paste("".to_string());
assert_eq!(event.as_paste_event(), Some(""));
assert_eq!(event.as_key_event(), None);
}
}
}