use anyhow::Result;
use crossterm::event::{self, Event as CrosstermEvent, KeyEvent, MouseEvent};
use std::time::Duration;
use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
use tokio::time::interval;
use tracing::{debug, error, warn};
#[derive(Debug, Clone)]
pub enum Event {
Key(KeyEvent),
Mouse(MouseEvent),
Tick,
Resize(u16, u16),
Quit,
Custom(String),
}
pub struct EventHandler {
receiver: UnboundedReceiver<Event>,
sender: UnboundedSender<Event>,
_task: tokio::task::JoinHandle<()>,
}
impl EventHandler {
pub fn new(tick_rate_ms: u64) -> Self {
let tick_rate = Duration::from_millis(tick_rate_ms);
let (sender, receiver) = mpsc::unbounded_channel();
debug!(
"Initializing event handler with tick rate: {}ms",
tick_rate_ms
);
let task_sender = sender.clone();
let task = tokio::spawn(async move {
Self::event_loop(task_sender, tick_rate).await;
});
Self {
receiver,
sender,
_task: task,
}
}
pub async fn next(&mut self) -> Result<Event> {
if let Some(event) = self.receiver.recv().await {
debug!("Received event: {:?}", event);
Ok(event)
} else {
error!("Event channel closed unexpectedly");
Err(anyhow::anyhow!("Event channel closed"))
}
}
pub fn send(&self, event: Event) -> Result<()> {
match self.sender.send(event) {
Ok(()) => Ok(()),
Err(e) => {
error!("Failed to send event: {}", e);
Err(anyhow::anyhow!("Failed to send event: {}", e))
}
}
}
#[must_use]
pub fn has_pending_events(&self) -> bool {
!self.receiver.is_empty()
}
#[must_use]
pub fn pending_event_count(&self) -> usize {
if self.receiver.is_empty() {
0
} else {
1
}
}
pub fn quit(&self) -> Result<()> {
debug!("Sending quit event");
self.send(Event::Quit)
}
pub fn send_custom(&self, message: String) -> Result<()> {
debug!("Sending custom event: {}", message);
self.send(Event::Custom(message))
}
async fn event_loop(sender: UnboundedSender<Event>, tick_rate: Duration) {
debug!("Starting event processing loop");
let mut tick_interval = interval(tick_rate);
let mut event_count = 0u64;
let mut last_resize = None;
loop {
tokio::select! {
_ = tick_interval.tick() => {
event_count += 1;
if event_count % 100 == 0 {
debug!("Processed {} events", event_count);
}
if let Err(e) = sender.send(Event::Tick) {
error!("Failed to send tick event: {}", e);
break;
}
}
event_result = Self::read_terminal_event() => {
match event_result {
Ok(Some(event)) => {
if let Event::Resize(w, h) = &event {
if last_resize == Some((*w, *h)) {
continue; }
last_resize = Some((*w, *h));
debug!("Terminal resized to {}x{}", w, h);
}
if let Err(e) = sender.send(event) {
error!("Failed to send terminal event: {}", e);
break;
}
}
Ok(None) => {
}
Err(e) => {
error!("Error reading terminal event: {}", e);
}
}
}
}
}
debug!("Event processing loop terminated");
}
async fn read_terminal_event() -> Result<Option<Event>> {
if !event::poll(Duration::from_millis(1))? {
return Ok(None);
}
match event::read()? {
CrosstermEvent::Key(key_event) => {
if key_event.kind == event::KeyEventKind::Press {
Ok(Some(Event::Key(key_event)))
} else {
Ok(None)
}
}
CrosstermEvent::Mouse(mouse_event) => Ok(Some(Event::Mouse(mouse_event))),
CrosstermEvent::Resize(width, height) => Ok(Some(Event::Resize(width, height))),
CrosstermEvent::FocusGained => {
debug!("Terminal focus gained");
Ok(Some(Event::Custom("focus_gained".to_string())))
}
CrosstermEvent::FocusLost => {
debug!("Terminal focus lost");
Ok(Some(Event::Custom("focus_lost".to_string())))
}
CrosstermEvent::Paste(text) => {
debug!("Text pasted: {} characters", text.len());
Ok(Some(Event::Custom(format!("paste:{text}"))))
}
}
}
#[must_use]
pub fn new_smooth() -> Self {
Self::new(16) }
#[must_use]
pub fn new_efficient() -> Self {
Self::new(33) }
pub fn new_debug() -> Self {
debug!("Creating debug event handler");
Self::new(100) }
pub async fn flush_events(&mut self) -> usize {
let mut count = 0;
while let Ok(event) =
tokio::time::timeout(Duration::from_millis(1), self.receiver.recv()).await
{
if event.is_some() {
count += 1;
} else {
break;
}
}
debug!("Flushed {} events from queue", count);
count
}
pub async fn wait_for_event<F>(&mut self, timeout: Duration, mut predicate: F) -> Result<Event>
where
F: FnMut(&Event) -> bool,
{
let deadline = tokio::time::Instant::now() + timeout;
while tokio::time::Instant::now() < deadline {
match tokio::time::timeout(Duration::from_millis(10), self.next()).await {
Ok(Ok(event)) => {
if predicate(&event) {
return Ok(event);
}
}
Ok(Err(e)) => return Err(e),
Err(_) => {
}
}
}
Err(anyhow::anyhow!("Timeout waiting for matching event"))
}
pub async fn next_batch(&mut self, max_events: usize, timeout: Duration) -> Result<Vec<Event>> {
let mut events = Vec::new();
let deadline = tokio::time::Instant::now() + timeout;
while events.len() < max_events && tokio::time::Instant::now() < deadline {
match tokio::time::timeout(Duration::from_millis(1), self.next()).await {
Ok(Ok(event)) => {
events.push(event);
}
Ok(Err(e)) => return Err(e),
Err(_) => {
break;
}
}
}
debug!("Collected {} events in batch", events.len());
Ok(events)
}
#[must_use]
pub fn statistics(&self) -> String {
format!(
"Event Handler Statistics:\n\
- Queue empty: {}\n\
- Approximate pending: {}",
self.receiver.is_empty(),
self.pending_event_count()
)
}
}
impl Drop for EventHandler {
fn drop(&mut self) {
debug!("Event handler being dropped, cleaning up resources");
}
}
pub mod utils {
use super::KeyEvent;
use crossterm::event::{KeyCode, KeyModifiers};
#[must_use]
pub fn is_quit_event(key_event: &KeyEvent) -> bool {
matches!(
(key_event.code, key_event.modifiers),
(KeyCode::Char('q') | KeyCode::Esc, KeyModifiers::NONE)
| (KeyCode::Char('c'), KeyModifiers::CONTROL)
)
}
#[must_use]
pub fn is_refresh_event(key_event: &KeyEvent) -> bool {
matches!(
(key_event.code, key_event.modifiers),
(KeyCode::Char('r') | KeyCode::F(5), KeyModifiers::NONE)
| (KeyCode::Char('r'), KeyModifiers::CONTROL)
)
}
#[must_use]
pub fn is_help_event(key_event: &KeyEvent) -> bool {
matches!(
(key_event.code, key_event.modifiers),
(KeyCode::F(1) | KeyCode::Char('?' | 'h'), KeyModifiers::NONE)
)
}
#[must_use]
pub fn key_event_to_string(key_event: &KeyEvent) -> String {
let mut result = String::new();
if key_event.modifiers.contains(KeyModifiers::CONTROL) {
result.push_str("Ctrl+");
}
if key_event.modifiers.contains(KeyModifiers::ALT) {
result.push_str("Alt+");
}
if key_event.modifiers.contains(KeyModifiers::SHIFT) {
result.push_str("Shift+");
}
match key_event.code {
KeyCode::Char(c) => result.push(c),
KeyCode::F(n) => result.push_str(&format!("F{n}")),
KeyCode::Enter => result.push_str("Enter"),
KeyCode::Tab => result.push_str("Tab"),
KeyCode::Esc => result.push_str("Esc"),
KeyCode::Up => result.push_str("Up"),
KeyCode::Down => result.push_str("Down"),
KeyCode::Left => result.push_str("Left"),
KeyCode::Right => result.push_str("Right"),
KeyCode::Home => result.push_str("Home"),
KeyCode::End => result.push_str("End"),
KeyCode::PageUp => result.push_str("PageUp"),
KeyCode::PageDown => result.push_str("PageDown"),
KeyCode::Insert => result.push_str("Insert"),
KeyCode::Delete => result.push_str("Delete"),
KeyCode::Backspace => result.push_str("Backspace"),
_ => result.push_str(&format!("{:?}", key_event.code)),
}
result
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::time::sleep;
#[tokio::test]
async fn test_event_handler_creation() {
let mut handler = EventHandler::new(100);
let event = handler.next().await.unwrap();
assert!(matches!(event, Event::Tick));
}
#[tokio::test]
async fn test_custom_events() {
let handler = EventHandler::new(1000);
handler.send_custom("test_message".to_string()).unwrap();
}
#[tokio::test]
async fn test_quit_event() {
let handler = EventHandler::new(1000);
handler.quit().unwrap();
}
#[tokio::test]
async fn test_event_batch_processing() {
let mut handler = EventHandler::new(10);
sleep(Duration::from_millis(50)).await;
let events = handler
.next_batch(5, Duration::from_millis(100))
.await
.unwrap();
assert!(!events.is_empty());
}
}