use crate::action::Action;
use crossterm::event::{self, Event, MouseEventKind};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use tokio::sync::mpsc;
pub(crate) fn event_to_action(evt: Event) -> Option<Action> {
match evt {
Event::Key(key) if key.kind == crossterm::event::KeyEventKind::Press => {
Some(Action::RawKey(key))
}
Event::Resize(w, h) => Some(Action::Resize(w, h)),
Event::Mouse(m) if matches!(m.kind, MouseEventKind::Moved) => None,
Event::Mouse(m) => Some(Action::Mouse(m)),
_ => None,
}
}
pub struct EventHandler {
rx: mpsc::UnboundedReceiver<Action>,
stop: Arc<AtomicBool>,
}
impl EventHandler {
pub fn new() -> (Self, mpsc::UnboundedSender<Action>) {
let (tx, rx) = mpsc::unbounded_channel();
let event_tx = tx.clone();
let stop = Arc::new(AtomicBool::new(false));
let stop_clone = stop.clone();
std::thread::spawn(move || {
while !stop_clone.load(Ordering::Relaxed) {
if event::poll(Duration::from_millis(50)).unwrap_or(false)
&& let Ok(evt) = event::read()
&& let Some(action) = event_to_action(evt)
&& event_tx.send(action).is_err()
{
break;
}
}
});
(Self { rx, stop }, tx)
}
pub async fn next(&mut self) -> Option<Action> {
self.rx.recv().await
}
}
impl Drop for EventHandler {
fn drop(&mut self) {
self.stop.store(true, Ordering::Relaxed);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEvent};
#[test]
fn mouse_moved_events_are_dropped() {
for (col, row) in [(0u16, 0u16), (10, 5), (100, 50)] {
let evt = Event::Mouse(MouseEvent {
kind: MouseEventKind::Moved,
column: col,
row,
modifiers: KeyModifiers::empty(),
});
assert!(
event_to_action(evt).is_none(),
"Moved at ({col},{row}) must produce None — got Some"
);
}
}
#[test]
fn non_motion_mouse_events_pass_through() {
let cases = [
MouseEventKind::Down(MouseButton::Left),
MouseEventKind::Up(MouseButton::Left),
MouseEventKind::Drag(MouseButton::Left),
MouseEventKind::ScrollUp,
MouseEventKind::ScrollDown,
MouseEventKind::ScrollLeft,
MouseEventKind::ScrollRight,
];
for kind in cases {
let evt = Event::Mouse(MouseEvent {
kind,
column: 5,
row: 5,
modifiers: KeyModifiers::empty(),
});
let action = event_to_action(evt);
assert!(
matches!(action, Some(Action::Mouse(_))),
"non-motion mouse event {kind:?} must produce Some(Action::Mouse) — got something else"
);
}
}
#[test]
fn key_press_passes_release_drops() {
let press = Event::Key(KeyEvent::new_with_kind(
KeyCode::Char('a'),
KeyModifiers::empty(),
KeyEventKind::Press,
));
assert!(matches!(event_to_action(press), Some(Action::RawKey(_))));
let release = Event::Key(KeyEvent::new_with_kind(
KeyCode::Char('a'),
KeyModifiers::empty(),
KeyEventKind::Release,
));
assert!(event_to_action(release).is_none());
}
#[test]
fn resize_events_pass_through() {
let evt = Event::Resize(80, 24);
assert!(matches!(event_to_action(evt), Some(Action::Resize(80, 24))));
}
}