use crossterm::event::KeyCode;
pub use crossterm::event::{KeyModifiers, MouseButton, MouseEventKind};
#[derive(Debug, Clone, PartialEq)]
pub enum Event<M> {
Key(KeyEvent),
Mouse(MouseEvent),
Resize {
width: u16,
height: u16,
},
Tick,
User(M),
Quit,
Focus,
Blur,
Suspend,
Resume,
Paste(String),
#[doc(hidden)]
ExecProcess,
}
impl<M> Event<M> {
#[inline(always)]
pub const fn is_key(&self) -> bool {
matches!(self, Event::Key(_))
}
#[inline(always)]
pub fn is_key_press(&self, key: Key) -> bool {
matches!(self, Event::Key(k) if k.key == key)
}
pub fn is_key_with_modifiers(&self, key: Key, modifiers: KeyModifiers) -> bool {
matches!(self, Event::Key(k) if k.key == key && k.modifiers == modifiers)
}
#[inline(always)]
pub const fn as_key(&self) -> Option<&KeyEvent> {
match self {
Event::Key(k) => Some(k),
_ => None,
}
}
#[inline(always)]
pub const fn is_mouse(&self) -> bool {
matches!(self, Event::Mouse(_))
}
#[inline(always)]
pub const fn as_mouse(&self) -> Option<&MouseEvent> {
match self {
Event::Mouse(m) => Some(m),
_ => None,
}
}
pub fn is_click(&self) -> bool {
matches!(self, Event::Mouse(m) if m.is_click())
}
pub fn as_click(&self) -> Option<(u16, u16)> {
match self {
Event::Mouse(m) if m.is_click() => Some(m.position()),
_ => None,
}
}
pub fn is_resize(&self) -> bool {
matches!(self, Event::Resize { .. })
}
pub fn as_resize(&self) -> Option<(u16, u16)> {
match self {
Event::Resize { width, height } => Some((*width, *height)),
_ => None,
}
}
#[inline(always)]
pub const fn is_user(&self) -> bool {
matches!(self, Event::User(_))
}
#[inline(always)]
pub const fn as_user(&self) -> Option<&M> {
match self {
Event::User(msg) => Some(msg),
_ => None,
}
}
pub fn into_user(self) -> Option<M> {
match self {
Event::User(msg) => Some(msg),
_ => None,
}
}
#[inline(always)]
pub const fn is_quit(&self) -> bool {
matches!(self, Event::Quit)
}
#[inline(always)]
pub const fn is_tick(&self) -> bool {
matches!(self, Event::Tick)
}
pub fn is_paste(&self) -> bool {
matches!(self, Event::Paste(_))
}
pub fn as_paste(&self) -> Option<&str> {
match self {
Event::Paste(text) => Some(text.as_str()),
_ => None,
}
}
pub fn is_focus(&self) -> bool {
matches!(self, Event::Focus)
}
pub fn is_blur(&self) -> bool {
matches!(self, Event::Blur)
}
pub fn is_suspend(&self) -> bool {
matches!(self, Event::Suspend)
}
pub fn is_resume(&self) -> bool {
matches!(self, Event::Resume)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WindowSize {
pub width: u16,
pub height: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KeyEvent {
pub key: Key,
pub modifiers: KeyModifiers,
}
impl KeyEvent {
pub fn new(key: Key, modifiers: KeyModifiers) -> Self {
Self { key, modifiers }
}
pub fn is_char(&self) -> bool {
matches!(self.key, Key::Char(_))
}
pub fn char(&self) -> Option<char> {
match self.key {
Key::Char(c) => Some(c),
_ => None,
}
}
pub fn is(&self, key: Key) -> bool {
self.key == key
}
pub fn is_with_modifiers(&self, key: Key, modifiers: KeyModifiers) -> bool {
self.key == key && self.modifiers == modifiers
}
pub fn is_ctrl(&self) -> bool {
self.modifiers.contains(KeyModifiers::CONTROL)
}
pub fn is_alt(&self) -> bool {
self.modifiers.contains(KeyModifiers::ALT)
}
pub fn is_shift(&self) -> bool {
self.modifiers.contains(KeyModifiers::SHIFT)
}
pub fn is_super(&self) -> bool {
self.modifiers.contains(KeyModifiers::SUPER)
}
pub fn is_navigation(&self) -> bool {
matches!(
self.key,
Key::Up
| Key::Down
| Key::Left
| Key::Right
| Key::Home
| Key::End
| Key::PageUp
| Key::PageDown
)
}
pub fn is_function_key(&self) -> bool {
matches!(self.key, Key::F(_))
}
pub fn is_media_key(&self) -> bool {
matches!(
self.key,
Key::MediaPlay
| Key::MediaPause
| Key::MediaPlayPause
| Key::MediaStop
| Key::MediaNext
| Key::MediaPrevious
| Key::MediaFastForward
| Key::MediaRewind
| Key::MediaVolumeUp
| Key::MediaVolumeDown
| Key::MediaMute
)
}
pub fn has_modifiers(&self) -> bool {
!self.modifiers.is_empty()
}
pub fn no_modifiers(&self) -> bool {
self.modifiers.is_empty()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ModifierKey {
Shift,
Control,
Alt,
Super,
Meta,
Hyper,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Key {
Char(char),
Backspace,
Enter,
Left,
Right,
Up,
Down,
Home,
End,
PageUp,
PageDown,
Tab,
Delete,
Insert,
Esc,
F(u8),
Null,
CapsLock,
ScrollLock,
NumLock,
PrintScreen,
Pause,
Menu,
KeypadBegin,
MediaPlay,
MediaPause,
MediaPlayPause,
MediaStop,
MediaNext,
MediaPrevious,
MediaFastForward,
MediaRewind,
MediaVolumeUp,
MediaVolumeDown,
MediaMute,
Modifier(ModifierKey),
}
impl From<crossterm::event::KeyEvent> for KeyEvent {
fn from(event: crossterm::event::KeyEvent) -> Self {
let key = match event.code {
KeyCode::Char(c) => Key::Char(c),
KeyCode::Backspace => Key::Backspace,
KeyCode::Enter => Key::Enter,
KeyCode::Left => Key::Left,
KeyCode::Right => Key::Right,
KeyCode::Up => Key::Up,
KeyCode::Down => Key::Down,
KeyCode::Home => Key::Home,
KeyCode::End => Key::End,
KeyCode::PageUp => Key::PageUp,
KeyCode::PageDown => Key::PageDown,
KeyCode::Tab => Key::Tab,
KeyCode::BackTab => Key::Tab, KeyCode::Delete => Key::Delete,
KeyCode::Insert => Key::Insert,
KeyCode::Esc => Key::Esc,
KeyCode::F(n) => Key::F(n),
KeyCode::Null => Key::Null,
KeyCode::CapsLock => Key::CapsLock,
KeyCode::ScrollLock => Key::ScrollLock,
KeyCode::NumLock => Key::NumLock,
KeyCode::PrintScreen => Key::PrintScreen,
KeyCode::Pause => Key::Pause,
KeyCode::Menu => Key::Menu,
KeyCode::KeypadBegin => Key::KeypadBegin,
KeyCode::Media(crossterm::event::MediaKeyCode::Play) => Key::MediaPlay,
KeyCode::Media(crossterm::event::MediaKeyCode::Pause) => Key::MediaPause,
KeyCode::Media(crossterm::event::MediaKeyCode::PlayPause) => Key::MediaPlayPause,
KeyCode::Media(crossterm::event::MediaKeyCode::Stop) => Key::MediaStop,
KeyCode::Media(crossterm::event::MediaKeyCode::FastForward) => Key::MediaFastForward,
KeyCode::Media(crossterm::event::MediaKeyCode::Rewind) => Key::MediaRewind,
KeyCode::Media(crossterm::event::MediaKeyCode::TrackNext) => Key::MediaNext,
KeyCode::Media(crossterm::event::MediaKeyCode::TrackPrevious) => Key::MediaPrevious,
KeyCode::Media(crossterm::event::MediaKeyCode::LowerVolume) => Key::MediaVolumeDown,
KeyCode::Media(crossterm::event::MediaKeyCode::RaiseVolume) => Key::MediaVolumeUp,
KeyCode::Media(crossterm::event::MediaKeyCode::MuteVolume) => Key::MediaMute,
KeyCode::Modifier(
crossterm::event::ModifierKeyCode::LeftShift
| crossterm::event::ModifierKeyCode::RightShift,
) => Key::Modifier(ModifierKey::Shift),
KeyCode::Modifier(
crossterm::event::ModifierKeyCode::LeftControl
| crossterm::event::ModifierKeyCode::RightControl,
) => Key::Modifier(ModifierKey::Control),
KeyCode::Modifier(
crossterm::event::ModifierKeyCode::LeftAlt
| crossterm::event::ModifierKeyCode::RightAlt,
) => Key::Modifier(ModifierKey::Alt),
KeyCode::Modifier(
crossterm::event::ModifierKeyCode::LeftSuper
| crossterm::event::ModifierKeyCode::RightSuper,
) => Key::Modifier(ModifierKey::Super),
KeyCode::Modifier(
crossterm::event::ModifierKeyCode::LeftMeta
| crossterm::event::ModifierKeyCode::RightMeta,
) => Key::Modifier(ModifierKey::Meta),
KeyCode::Modifier(
crossterm::event::ModifierKeyCode::LeftHyper
| crossterm::event::ModifierKeyCode::RightHyper,
) => Key::Modifier(ModifierKey::Hyper),
_ => Key::Null, };
Self {
key,
modifiers: event.modifiers,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MouseEvent {
pub kind: MouseEventKind,
pub column: u16,
pub row: u16,
pub modifiers: KeyModifiers,
}
impl MouseEvent {
pub fn new(kind: MouseEventKind, column: u16, row: u16, modifiers: KeyModifiers) -> Self {
Self {
kind,
column,
row,
modifiers,
}
}
pub fn is_left_click(&self) -> bool {
matches!(self.kind, MouseEventKind::Down(MouseButton::Left))
}
pub fn is_right_click(&self) -> bool {
matches!(self.kind, MouseEventKind::Down(MouseButton::Right))
}
pub fn is_middle_click(&self) -> bool {
matches!(self.kind, MouseEventKind::Down(MouseButton::Middle))
}
pub fn is_click(&self) -> bool {
matches!(self.kind, MouseEventKind::Down(_))
}
pub fn is_release(&self) -> bool {
matches!(self.kind, MouseEventKind::Up(_))
}
pub fn is_drag(&self) -> bool {
matches!(self.kind, MouseEventKind::Drag(_))
}
pub fn is_left_drag(&self) -> bool {
matches!(self.kind, MouseEventKind::Drag(MouseButton::Left))
}
pub fn is_right_drag(&self) -> bool {
matches!(self.kind, MouseEventKind::Drag(MouseButton::Right))
}
pub fn is_middle_drag(&self) -> bool {
matches!(self.kind, MouseEventKind::Drag(MouseButton::Middle))
}
pub fn is_scroll_up(&self) -> bool {
matches!(self.kind, MouseEventKind::ScrollUp)
}
pub fn is_scroll_down(&self) -> bool {
matches!(self.kind, MouseEventKind::ScrollDown)
}
pub fn is_scroll_left(&self) -> bool {
matches!(self.kind, MouseEventKind::ScrollLeft)
}
pub fn is_scroll_right(&self) -> bool {
matches!(self.kind, MouseEventKind::ScrollRight)
}
pub fn is_scroll(&self) -> bool {
matches!(
self.kind,
MouseEventKind::ScrollUp
| MouseEventKind::ScrollDown
| MouseEventKind::ScrollLeft
| MouseEventKind::ScrollRight
)
}
pub fn is_move(&self) -> bool {
matches!(self.kind, MouseEventKind::Moved)
}
pub fn button(&self) -> Option<MouseButton> {
match self.kind {
MouseEventKind::Down(btn) | MouseEventKind::Up(btn) | MouseEventKind::Drag(btn) => {
Some(btn)
}
_ => None,
}
}
pub fn position(&self) -> (u16, u16) {
(self.column, self.row)
}
pub fn is_within(&self, x: u16, y: u16, width: u16, height: u16) -> bool {
self.column >= x && self.column < x + width && self.row >= y && self.row < y + height
}
pub fn is_at(&self, column: u16, row: u16) -> bool {
self.column == column && self.row == row
}
pub fn has_modifier(&self, modifier: KeyModifiers) -> bool {
self.modifiers.contains(modifier)
}
pub fn is_ctrl(&self) -> bool {
self.modifiers.contains(KeyModifiers::CONTROL)
}
pub fn is_alt(&self) -> bool {
self.modifiers.contains(KeyModifiers::ALT)
}
pub fn is_shift(&self) -> bool {
self.modifiers.contains(KeyModifiers::SHIFT)
}
pub fn has_modifiers(&self) -> bool {
!self.modifiers.is_empty()
}
pub fn no_modifiers(&self) -> bool {
self.modifiers.is_empty()
}
}
impl From<crossterm::event::MouseEvent> for MouseEvent {
fn from(event: crossterm::event::MouseEvent) -> Self {
Self {
kind: event.kind,
column: event.column,
row: event.row,
modifiers: event.modifiers,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
#[test]
fn test_key_event_creation() {
let event = KeyEvent::new(Key::Char('a'), KeyModifiers::empty());
assert_eq!(event.key, Key::Char('a'));
assert!(event.is_char());
assert_eq!(event.char(), Some('a'));
}
#[test]
fn test_key_event_modifiers() {
let event = KeyEvent::new(Key::Char('c'), KeyModifiers::CONTROL);
assert_eq!(event.key, Key::Char('c'));
assert_eq!(event.modifiers, KeyModifiers::CONTROL);
}
#[test]
fn test_non_char_keys() {
let event = KeyEvent::new(Key::Enter, KeyModifiers::empty());
assert!(!event.is_char());
assert_eq!(event.char(), None);
}
#[test]
fn test_window_size_creation() {
let size = WindowSize {
width: 80,
height: 24,
};
assert_eq!(size.width, 80);
assert_eq!(size.height, 24);
}
#[test]
fn test_mouse_event_creation() {
let event = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 10,
row: 5,
modifiers: KeyModifiers::empty(),
};
assert_eq!(event.column, 10);
assert_eq!(event.row, 5);
}
#[test]
fn test_event_variants() {
let key_event = Event::<String>::Key(KeyEvent::new(Key::Char('a'), KeyModifiers::empty()));
let mouse_event = Event::<String>::Mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 0,
row: 0,
modifiers: KeyModifiers::empty(),
});
let resize_event = Event::<String>::Resize {
width: 80,
height: 24,
};
let tick_event = Event::<String>::Tick;
let user_event = Event::User("test".to_string());
let quit_event = Event::<String>::Quit;
let focus_event = Event::<String>::Focus;
let blur_event = Event::<String>::Blur;
let suspend_event = Event::<String>::Suspend;
let resume_event = Event::<String>::Resume;
let paste_event = Event::<String>::Paste("pasted text".to_string());
assert!(key_event.is_key());
assert!(key_event.as_key().is_some());
assert!(mouse_event.is_mouse());
assert!(mouse_event.as_mouse().is_some());
assert!(resize_event.is_resize());
assert_eq!(resize_event.as_resize(), Some((80, 24)));
assert!(tick_event.is_tick());
assert!(user_event.is_user());
assert_eq!(user_event.as_user(), Some(&"test".to_string()));
assert!(quit_event.is_quit());
assert!(focus_event.is_focus());
assert!(blur_event.is_blur());
assert!(suspend_event.is_suspend());
assert!(resume_event.is_resume());
assert!(paste_event.is_paste());
assert_eq!(paste_event.as_paste(), Some("pasted text"));
}
#[test]
fn test_key_variants() {
let keys = vec![
Key::Char('a'),
Key::Backspace,
Key::Enter,
Key::Left,
Key::Right,
Key::Up,
Key::Down,
Key::Home,
Key::End,
Key::PageUp,
Key::PageDown,
Key::Tab,
Key::Delete,
Key::Insert,
Key::Esc,
Key::F(1),
Key::Null,
];
for key in keys {
let event = KeyEvent::new(key, KeyModifiers::empty());
assert_eq!(event.key, key);
match key {
Key::Char(c) => {
assert!(event.is_char());
assert_eq!(event.char(), Some(c));
}
_ => {
assert!(!event.is_char());
assert_eq!(event.char(), None);
}
}
}
}
#[test]
fn test_crossterm_key_conversion() {
let crossterm_event = crossterm::event::KeyEvent::new(
KeyCode::Char('x'),
KeyModifiers::CONTROL | KeyModifiers::SHIFT,
);
let key_event: KeyEvent = crossterm_event.into();
assert_eq!(key_event.key, Key::Char('x'));
assert_eq!(
key_event.modifiers,
KeyModifiers::CONTROL | KeyModifiers::SHIFT
);
}
#[test]
fn test_enhanced_keys() {
use crossterm::event::{KeyCode, KeyModifiers};
let caps_lock = crossterm::event::KeyEvent::new(KeyCode::CapsLock, KeyModifiers::empty());
let key_event = KeyEvent::from(caps_lock);
assert_eq!(key_event.key, Key::CapsLock);
let scroll_lock =
crossterm::event::KeyEvent::new(KeyCode::ScrollLock, KeyModifiers::empty());
let key_event = KeyEvent::from(scroll_lock);
assert_eq!(key_event.key, Key::ScrollLock);
let print_screen =
crossterm::event::KeyEvent::new(KeyCode::PrintScreen, KeyModifiers::empty());
let key_event = KeyEvent::from(print_screen);
assert_eq!(key_event.key, Key::PrintScreen);
let play = crossterm::event::KeyEvent::new(
KeyCode::Media(crossterm::event::MediaKeyCode::Play),
KeyModifiers::empty(),
);
let key_event = KeyEvent::from(play);
assert_eq!(key_event.key, Key::MediaPlay);
let volume_up = crossterm::event::KeyEvent::new(
KeyCode::Media(crossterm::event::MediaKeyCode::RaiseVolume),
KeyModifiers::empty(),
);
let key_event = KeyEvent::from(volume_up);
assert_eq!(key_event.key, Key::MediaVolumeUp);
let left_shift = crossterm::event::KeyEvent::new(
KeyCode::Modifier(crossterm::event::ModifierKeyCode::LeftShift),
KeyModifiers::empty(),
);
let key_event = KeyEvent::from(left_shift);
assert_eq!(key_event.key, Key::Modifier(ModifierKey::Shift));
let right_alt = crossterm::event::KeyEvent::new(
KeyCode::Modifier(crossterm::event::ModifierKeyCode::RightAlt),
KeyModifiers::empty(),
);
let key_event = KeyEvent::from(right_alt);
assert_eq!(key_event.key, Key::Modifier(ModifierKey::Alt));
}
#[test]
fn test_crossterm_key_conversion_all_keys() {
let test_cases = vec![
(KeyCode::Backspace, Key::Backspace),
(KeyCode::Enter, Key::Enter),
(KeyCode::Left, Key::Left),
(KeyCode::Right, Key::Right),
(KeyCode::Up, Key::Up),
(KeyCode::Down, Key::Down),
(KeyCode::Home, Key::Home),
(KeyCode::End, Key::End),
(KeyCode::PageUp, Key::PageUp),
(KeyCode::PageDown, Key::PageDown),
(KeyCode::Tab, Key::Tab),
(KeyCode::Delete, Key::Delete),
(KeyCode::Insert, Key::Insert),
(KeyCode::Esc, Key::Esc),
(KeyCode::F(1), Key::F(1)),
(KeyCode::F(12), Key::F(12)),
(KeyCode::Null, Key::Null),
(KeyCode::BackTab, Key::Tab), ];
for (crossterm_code, expected_key) in test_cases {
let crossterm_event =
crossterm::event::KeyEvent::new(crossterm_code, KeyModifiers::empty());
let key_event: KeyEvent = crossterm_event.into();
assert_eq!(key_event.key, expected_key);
}
}
#[test]
fn test_crossterm_mouse_conversion() {
let crossterm_event = crossterm::event::MouseEvent {
kind: MouseEventKind::Down(MouseButton::Right),
column: 42,
row: 24,
modifiers: KeyModifiers::ALT,
};
let mouse_event: MouseEvent = crossterm_event.into();
assert_eq!(mouse_event.kind, MouseEventKind::Down(MouseButton::Right));
assert_eq!(mouse_event.column, 42);
assert_eq!(mouse_event.row, 24);
assert_eq!(mouse_event.modifiers, KeyModifiers::ALT);
}
proptest! {
#[test]
fn test_key_event_properties(
c in any::<char>(),
ctrl in any::<bool>(),
alt in any::<bool>(),
shift in any::<bool>()
) {
let mut modifiers = KeyModifiers::empty();
if ctrl { modifiers |= KeyModifiers::CONTROL; }
if alt { modifiers |= KeyModifiers::ALT; }
if shift { modifiers |= KeyModifiers::SHIFT; }
let event = KeyEvent::new(Key::Char(c), modifiers);
prop_assert_eq!(event.key, Key::Char(c));
prop_assert_eq!(event.modifiers, modifiers);
prop_assert!(event.is_char());
prop_assert_eq!(event.char(), Some(c));
}
#[test]
fn test_window_size_properties(
width in 1u16..1000u16,
height in 1u16..1000u16
) {
let size = WindowSize { width, height };
prop_assert_eq!(size.width, width);
prop_assert_eq!(size.height, height);
let size2 = size;
prop_assert_eq!(size, size2);
}
#[test]
fn test_mouse_event_properties(
column in 0u16..1000u16,
row in 0u16..1000u16,
button_idx in 0..3usize
) {
let button = match button_idx {
0 => MouseButton::Left,
1 => MouseButton::Right,
2 => MouseButton::Middle,
_ => unreachable!(),
};
let event = MouseEvent {
kind: MouseEventKind::Down(button),
column,
row,
modifiers: KeyModifiers::empty(),
};
prop_assert_eq!(event.column, column);
prop_assert_eq!(event.row, row);
prop_assert_eq!(event.kind, MouseEventKind::Down(button));
}
#[test]
fn test_event_user_message_properties(
message in ".*"
) {
let event = Event::User(message.clone());
prop_assert!(event.is_user());
prop_assert_eq!(event.as_user(), Some(&message));
}
#[test]
fn test_event_resize_properties(
width in 1u16..1000u16,
height in 1u16..1000u16
) {
let event = Event::<String>::Resize { width, height };
prop_assert!(event.is_resize());
prop_assert_eq!(event.as_resize(), Some((width, height)));
}
#[test]
fn test_event_paste_properties(
paste_text in ".*"
) {
let event = Event::<String>::Paste(paste_text.clone());
prop_assert!(event.is_paste());
prop_assert_eq!(event.as_paste(), Some(paste_text.as_str()));
}
#[test]
fn test_function_key_properties(
key_num in 1u8..25u8
) {
let key = Key::F(key_num);
let event = KeyEvent::new(key, KeyModifiers::empty());
prop_assert_eq!(event.key, Key::F(key_num));
prop_assert!(!event.is_char());
prop_assert_eq!(event.char(), None);
}
#[test]
fn test_key_modifier_combinations(
ctrl in any::<bool>(),
alt in any::<bool>(),
shift in any::<bool>(),
super_key in any::<bool>()
) {
let mut modifiers = KeyModifiers::empty();
if ctrl { modifiers |= KeyModifiers::CONTROL; }
if alt { modifiers |= KeyModifiers::ALT; }
if shift { modifiers |= KeyModifiers::SHIFT; }
if super_key { modifiers |= KeyModifiers::SUPER; }
let event = KeyEvent::new(Key::Char('a'), modifiers);
prop_assert_eq!(event.modifiers.contains(KeyModifiers::CONTROL), ctrl);
prop_assert_eq!(event.modifiers.contains(KeyModifiers::ALT), alt);
prop_assert_eq!(event.modifiers.contains(KeyModifiers::SHIFT), shift);
prop_assert_eq!(event.modifiers.contains(KeyModifiers::SUPER), super_key);
}
}
}