use crossterm::event::{
KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum TerminalInputEvent {
Key(TerminalKeyEvent),
Mouse(TerminalMouseEvent),
Resize { cols: u16, rows: u16 },
Bytes(Vec<u8>),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TerminalKeyEvent {
pub code: TerminalKeyCode,
pub kind: TerminalKeyPhase,
pub modifiers: TerminalModifiers,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum TerminalKeyCode {
Backspace,
Enter,
Left,
Right,
Up,
Down,
Home,
End,
PageUp,
PageDown,
Tab,
BackTab,
Delete,
Insert,
Function(u8),
Char(char),
Null,
Esc,
CapsLock,
ScrollLock,
NumLock,
PrintScreen,
Pause,
Menu,
KeypadBegin,
Media(String),
Modifier(String),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TerminalKeyPhase {
Press,
Repeat,
Release,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct TerminalModifiers {
pub shift: bool,
pub control: bool,
pub alt: bool,
pub super_key: bool,
pub hyper: bool,
pub meta: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TerminalMouseEvent {
pub phase: TerminalMousePhase,
pub button: Option<TerminalMouseButton>,
pub col: u16,
pub row: u16,
pub modifiers: TerminalModifiers,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TerminalMousePhase {
Down,
Up,
Drag,
Move,
ScrollUp,
ScrollDown,
ScrollLeft,
ScrollRight,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TerminalMouseButton {
Left,
Right,
Middle,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TerminalGeometry {
pub cols: u16,
pub rows: u16,
}
impl TerminalInputEvent {
#[must_use]
pub fn from_crossterm_event(event: crossterm::event::Event) -> Option<Self> {
match event {
crossterm::event::Event::Key(key) => Some(Self::Key(TerminalKeyEvent::from(key))),
crossterm::event::Event::Mouse(mouse) => {
Some(Self::Mouse(TerminalMouseEvent::from(mouse)))
}
crossterm::event::Event::Resize(cols, rows) => Some(Self::Resize { cols, rows }),
crossterm::event::Event::Paste(text) => Some(Self::Bytes(text.into_bytes())),
crossterm::event::Event::FocusGained | crossterm::event::Event::FocusLost => None,
}
}
}
impl TerminalKeyEvent {
#[must_use]
pub fn to_crossterm(&self) -> Option<KeyEvent> {
let code = self.code.to_crossterm()?;
Some(KeyEvent::new_with_kind(
code,
self.modifiers.to_crossterm(),
self.kind.to_crossterm(),
))
}
}
impl From<KeyEvent> for TerminalKeyEvent {
fn from(event: KeyEvent) -> Self {
Self {
code: TerminalKeyCode::from(event.code),
kind: TerminalKeyPhase::from(event.kind),
modifiers: TerminalModifiers::from(event.modifiers),
}
}
}
impl TerminalKeyCode {
#[must_use]
pub const fn to_crossterm(&self) -> Option<KeyCode> {
Some(match self {
Self::Backspace => KeyCode::Backspace,
Self::Enter => KeyCode::Enter,
Self::Left => KeyCode::Left,
Self::Right => KeyCode::Right,
Self::Up => KeyCode::Up,
Self::Down => KeyCode::Down,
Self::Home => KeyCode::Home,
Self::End => KeyCode::End,
Self::PageUp => KeyCode::PageUp,
Self::PageDown => KeyCode::PageDown,
Self::Tab => KeyCode::Tab,
Self::BackTab => KeyCode::BackTab,
Self::Delete => KeyCode::Delete,
Self::Insert => KeyCode::Insert,
Self::Function(value) => KeyCode::F(*value),
Self::Char(value) => KeyCode::Char(*value),
Self::Null => KeyCode::Null,
Self::Esc => KeyCode::Esc,
Self::CapsLock => KeyCode::CapsLock,
Self::ScrollLock => KeyCode::ScrollLock,
Self::NumLock => KeyCode::NumLock,
Self::PrintScreen => KeyCode::PrintScreen,
Self::Pause => KeyCode::Pause,
Self::Menu => KeyCode::Menu,
Self::KeypadBegin => KeyCode::KeypadBegin,
Self::Media(_) | Self::Modifier(_) => return None,
})
}
}
impl From<KeyCode> for TerminalKeyCode {
fn from(code: KeyCode) -> Self {
match code {
KeyCode::Backspace => Self::Backspace,
KeyCode::Enter => Self::Enter,
KeyCode::Left => Self::Left,
KeyCode::Right => Self::Right,
KeyCode::Up => Self::Up,
KeyCode::Down => Self::Down,
KeyCode::Home => Self::Home,
KeyCode::End => Self::End,
KeyCode::PageUp => Self::PageUp,
KeyCode::PageDown => Self::PageDown,
KeyCode::Tab => Self::Tab,
KeyCode::BackTab => Self::BackTab,
KeyCode::Delete => Self::Delete,
KeyCode::Insert => Self::Insert,
KeyCode::F(index) => Self::Function(index),
KeyCode::Char(value) => Self::Char(value),
KeyCode::Null => Self::Null,
KeyCode::Esc => Self::Esc,
KeyCode::CapsLock => Self::CapsLock,
KeyCode::ScrollLock => Self::ScrollLock,
KeyCode::NumLock => Self::NumLock,
KeyCode::PrintScreen => Self::PrintScreen,
KeyCode::Pause => Self::Pause,
KeyCode::Menu => Self::Menu,
KeyCode::KeypadBegin => Self::KeypadBegin,
KeyCode::Media(value) => Self::Media(format!("{value:?}")),
KeyCode::Modifier(value) => Self::Modifier(format!("{value:?}")),
}
}
}
impl TerminalKeyPhase {
#[must_use]
pub const fn to_crossterm(self) -> KeyEventKind {
match self {
Self::Press => KeyEventKind::Press,
Self::Repeat => KeyEventKind::Repeat,
Self::Release => KeyEventKind::Release,
}
}
}
impl From<KeyEventKind> for TerminalKeyPhase {
fn from(kind: KeyEventKind) -> Self {
match kind {
KeyEventKind::Press => Self::Press,
KeyEventKind::Repeat => Self::Repeat,
KeyEventKind::Release => Self::Release,
}
}
}
impl TerminalMouseEvent {
#[must_use]
pub fn to_crossterm(self) -> Option<MouseEvent> {
let kind = match self.phase {
TerminalMousePhase::Down => MouseEventKind::Down(self.button?.to_crossterm()),
TerminalMousePhase::Up => MouseEventKind::Up(self.button?.to_crossterm()),
TerminalMousePhase::Drag => MouseEventKind::Drag(self.button?.to_crossterm()),
TerminalMousePhase::Move => MouseEventKind::Moved,
TerminalMousePhase::ScrollUp => MouseEventKind::ScrollUp,
TerminalMousePhase::ScrollDown => MouseEventKind::ScrollDown,
TerminalMousePhase::ScrollLeft => MouseEventKind::ScrollLeft,
TerminalMousePhase::ScrollRight => MouseEventKind::ScrollRight,
};
Some(MouseEvent {
kind,
column: self.col,
row: self.row,
modifiers: self.modifiers.to_crossterm(),
})
}
}
impl From<MouseEvent> for TerminalMouseEvent {
fn from(event: MouseEvent) -> Self {
let (phase, button) = match event.kind {
MouseEventKind::Down(button) => (TerminalMousePhase::Down, Some(button.into())),
MouseEventKind::Up(button) => (TerminalMousePhase::Up, Some(button.into())),
MouseEventKind::Drag(button) => (TerminalMousePhase::Drag, Some(button.into())),
MouseEventKind::Moved => (TerminalMousePhase::Move, None),
MouseEventKind::ScrollUp => (TerminalMousePhase::ScrollUp, None),
MouseEventKind::ScrollDown => (TerminalMousePhase::ScrollDown, None),
MouseEventKind::ScrollLeft => (TerminalMousePhase::ScrollLeft, None),
MouseEventKind::ScrollRight => (TerminalMousePhase::ScrollRight, None),
};
Self {
phase,
button,
col: event.column,
row: event.row,
modifiers: TerminalModifiers::from(event.modifiers),
}
}
}
impl TerminalMouseButton {
#[must_use]
pub const fn to_crossterm(self) -> MouseButton {
match self {
Self::Left => MouseButton::Left,
Self::Right => MouseButton::Right,
Self::Middle => MouseButton::Middle,
}
}
}
impl From<MouseButton> for TerminalMouseButton {
fn from(button: MouseButton) -> Self {
match button {
MouseButton::Left => Self::Left,
MouseButton::Right => Self::Right,
MouseButton::Middle => Self::Middle,
}
}
}
impl TerminalModifiers {
#[must_use]
pub fn to_crossterm(self) -> KeyModifiers {
let mut modifiers = KeyModifiers::empty();
if self.shift {
modifiers.insert(KeyModifiers::SHIFT);
}
if self.control {
modifiers.insert(KeyModifiers::CONTROL);
}
if self.alt {
modifiers.insert(KeyModifiers::ALT);
}
if self.super_key {
modifiers.insert(KeyModifiers::SUPER);
}
if self.hyper {
modifiers.insert(KeyModifiers::HYPER);
}
if self.meta {
modifiers.insert(KeyModifiers::META);
}
modifiers
}
}
impl From<KeyModifiers> for TerminalModifiers {
fn from(modifiers: KeyModifiers) -> Self {
Self {
shift: modifiers.contains(KeyModifiers::SHIFT),
control: modifiers.contains(KeyModifiers::CONTROL),
alt: modifiers.contains(KeyModifiers::ALT),
super_key: modifiers.contains(KeyModifiers::SUPER),
hyper: modifiers.contains(KeyModifiers::HYPER),
meta: modifiers.contains(KeyModifiers::META),
}
}
}
#[cfg(test)]
mod tests {
use super::{
TerminalInputEvent, TerminalModifiers, TerminalMouseButton, TerminalMouseEvent,
TerminalMousePhase,
};
use crossterm::event::{Event, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
#[test]
fn maps_crossterm_mouse_down() {
let event = TerminalMouseEvent::from(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 4,
row: 9,
modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL,
});
assert_eq!(
event,
TerminalMouseEvent {
phase: TerminalMousePhase::Down,
button: Some(TerminalMouseButton::Left),
col: 4,
row: 9,
modifiers: TerminalModifiers {
shift: true,
control: true,
alt: false,
super_key: false,
hyper: false,
meta: false,
},
}
);
}
#[test]
fn maps_crossterm_mouse_up_and_drag() {
let up = TerminalMouseEvent::from(MouseEvent {
kind: MouseEventKind::Up(MouseButton::Right),
column: 5,
row: 7,
modifiers: KeyModifiers::empty(),
});
assert_eq!(up.phase, TerminalMousePhase::Up);
assert_eq!(up.button, Some(TerminalMouseButton::Right));
let drag = TerminalMouseEvent::from(MouseEvent {
kind: MouseEventKind::Drag(MouseButton::Middle),
column: 9,
row: 3,
modifiers: KeyModifiers::empty(),
});
assert_eq!(drag.phase, TerminalMousePhase::Drag);
assert_eq!(drag.button, Some(TerminalMouseButton::Middle));
}
#[test]
fn maps_crossterm_mouse_motion_and_scroll() {
let moved = TerminalMouseEvent::from(MouseEvent {
kind: MouseEventKind::Moved,
column: 8,
row: 2,
modifiers: KeyModifiers::empty(),
});
assert_eq!(moved.phase, TerminalMousePhase::Move);
assert_eq!(moved.button, None);
let scroll = TerminalMouseEvent::from(MouseEvent {
kind: MouseEventKind::ScrollDown,
column: 8,
row: 2,
modifiers: KeyModifiers::ALT,
});
assert_eq!(scroll.phase, TerminalMousePhase::ScrollDown);
assert!(scroll.modifiers.alt);
}
#[test]
fn maps_crossterm_event_resize() {
assert_eq!(
TerminalInputEvent::from_crossterm_event(Event::Resize(120, 30)),
Some(TerminalInputEvent::Resize {
cols: 120,
rows: 30
})
);
}
}