use std::io;
use std::pin::Pin;
use crossterm::event::{Event as CrosstermEvent, EventStream, KeyEventKind};
use futures::{Stream, StreamExt};
use tokio::sync::mpsc;
use tokio::sync::mpsc::error::TryRecvError;
use crate::components::events::{AppEvent, AppTx, InputEvent};
pub type InputSource = Pin<Box<dyn Stream<Item = AppEvent> + Send>>;
pub struct EventHandler {
tx: AppTx,
rx: mpsc::UnboundedReceiver<AppEvent>,
input: InputSource,
}
impl Default for EventHandler {
fn default() -> Self {
Self::new()
}
}
impl EventHandler {
pub fn new() -> Self {
Self::from_input(crossterm_input())
}
pub fn from_input(input: impl Stream<Item = AppEvent> + Send + 'static) -> Self {
let (tx, rx) = mpsc::unbounded_channel();
Self {
tx,
rx,
input: Box::pin(input.fuse()),
}
}
pub fn app_sender(&self) -> AppTx {
self.tx.clone()
}
pub fn try_next(&mut self) -> Option<AppEvent> {
match self.rx.try_recv() {
Ok(msg) => Some(msg),
Err(TryRecvError::Empty) => None,
Err(TryRecvError::Disconnected) => {
unreachable!(
"EventHandler::tx is owned by this struct and the `&mut self` borrow \
guarantees it outlives this call; channel cannot be Disconnected here"
)
}
}
}
pub async fn next(&mut self) -> AppEvent {
tokio::select! {
biased;
Some(msg) = self.rx.recv() => msg,
event = self.input.next() => event.unwrap_or(AppEvent::Quit),
}
}
}
fn crossterm_input() -> impl Stream<Item = AppEvent> + Send {
EventStream::new().filter_map(|event| {
tracing::debug!("RAW EVENT: {:?}", event);
futures::future::ready(decode(event))
})
}
pub(crate) fn decode(event: io::Result<CrosstermEvent>) -> Option<AppEvent> {
match event {
Ok(CrosstermEvent::Key(key)) if key.kind != KeyEventKind::Release => {
Some(AppEvent::Input(InputEvent::Key(key)))
}
Ok(CrosstermEvent::Mouse(mouse)) => Some(AppEvent::Input(InputEvent::Mouse(mouse))),
Ok(CrosstermEvent::Paste(text)) => Some(AppEvent::Input(InputEvent::Paste(text))),
Ok(CrosstermEvent::Resize(_, _)) => Some(AppEvent::Redraw),
Ok(_) => None,
Err(e) => {
tracing::warn!("terminal input error: {e}");
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use futures::stream;
use ratatui::crossterm::event::{
Event as CrosstermEvent, KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers,
};
fn key(code: KeyCode, kind: KeyEventKind) -> CrosstermEvent {
CrosstermEvent::Key(KeyEvent {
code,
modifiers: KeyModifiers::NONE,
kind,
state: KeyEventState::NONE,
})
}
#[test]
fn decode_keeps_presses_and_drops_releases() {
assert!(matches!(
decode(Ok(key(KeyCode::Char('a'), KeyEventKind::Press))),
Some(AppEvent::Input(InputEvent::Key(k))) if k.code == KeyCode::Char('a')
));
assert!(decode(Ok(key(KeyCode::Char('a'), KeyEventKind::Release))).is_none());
}
#[test]
fn decode_turns_a_resize_into_a_redraw_and_skips_errors() {
assert!(matches!(
decode(Ok(CrosstermEvent::Resize(80, 24))),
Some(AppEvent::Redraw)
));
assert!(decode(Err(std::io::Error::other("hangup"))).is_none());
}
#[tokio::test]
async fn a_scripted_input_source_is_delivered_in_order() {
let mut events = EventHandler::from_input(stream::iter([
AppEvent::Redraw,
AppEvent::Input(InputEvent::Paste("p".into())),
]));
assert!(matches!(events.next().await, AppEvent::Redraw));
assert!(matches!(
events.next().await,
AppEvent::Input(InputEvent::Paste(s)) if s == "p"
));
}
#[tokio::test]
async fn app_messages_are_drained_before_input() {
let mut events = EventHandler::from_input(stream::iter([AppEvent::Redraw]));
events.app_sender().send(AppEvent::Quit).unwrap();
assert!(matches!(events.next().await, AppEvent::Quit));
assert!(matches!(events.next().await, AppEvent::Redraw));
}
#[tokio::test]
async fn an_exhausted_input_source_yields_quit() {
let mut events = EventHandler::from_input(stream::empty());
assert!(matches!(events.next().await, AppEvent::Quit));
assert!(matches!(events.next().await, AppEvent::Quit));
}
#[test]
fn try_next_only_peeks_the_app_channel() {
let mut events = EventHandler::from_input(stream::iter([AppEvent::Redraw]));
assert!(events.try_next().is_none());
events.app_sender().send(AppEvent::Redraw).unwrap();
assert!(matches!(events.try_next(), Some(AppEvent::Redraw)));
}
}