ratado 0.2.0

A fast, keyboard-driven terminal task manager built with Rust and Ratatui
Documentation
//! Event handling system for the application.
//!
//! This module provides the [`EventHandler`] which manages the event stream
//! for the application. It captures keyboard input, mouse events, resize events,
//! and generates periodic tick events for time-based updates.
//!
//! ## Architecture
//!
//! The event handler runs a background task that polls for crossterm events
//! and sends them through an async channel. The main loop receives these events
//! and processes them accordingly.
//!
//! ## Example
//!
//! ```rust,no_run
//! use std::time::Duration;
//! use ratado::handlers::events::{EventHandler, AppEvent};
//!
//! # async fn example() {
//! let mut events = EventHandler::new(Duration::from_millis(250));
//!
//! while let Some(event) = events.next().await {
//!     match event {
//!         AppEvent::Key(key) => { /* handle key */ }
//!         AppEvent::Tick => { /* update timers */ }
//!         _ => {}
//!     }
//! }
//! # }
//! ```

use crossterm::event::{self, Event as CrosstermEvent, KeyEvent, MouseEvent};
use std::time::{Duration, Instant};
use tokio::sync::mpsc;

/// Application events that drive the event loop.
///
/// These events are generated by the [`EventHandler`] and processed
/// by the main application loop.
#[derive(Debug, Clone)]
pub enum AppEvent {
    /// A keyboard key was pressed
    Key(KeyEvent),
    /// A mouse event occurred (click, scroll, etc.)
    Mouse(MouseEvent),
    /// A tick event for time-based updates
    Tick,
    /// The terminal was resized
    Resize(u16, u16),
}

/// Handles event polling and distribution.
///
/// The `EventHandler` spawns a background task that polls for terminal events
/// and tick events, sending them through an async channel for processing
/// by the main loop.
///
/// # Shutdown
///
/// The event handler will continue running until the receiver is dropped.
/// Dropping the `EventHandler` will stop the background polling task.
pub struct EventHandler {
    /// Receiver for events from the background task
    rx: mpsc::UnboundedReceiver<AppEvent>,
    /// Sender kept to prevent channel closure (dropped when EventHandler is dropped)
    #[allow(dead_code)]
    tx: mpsc::UnboundedSender<AppEvent>,
}

impl EventHandler {
    /// Creates a new EventHandler with the specified tick rate.
    ///
    /// Spawns a background task that polls for terminal events and generates
    /// tick events at the specified interval.
    ///
    /// # Arguments
    ///
    /// * `tick_rate` - How often tick events should be generated
    ///
    /// # Returns
    ///
    /// A new `EventHandler` ready to receive events.
    ///
    /// # Panics
    ///
    /// Panics if the background task fails to send events (channel closed unexpectedly).
    pub fn new(tick_rate: Duration) -> Self {
        let (tx, rx) = mpsc::unbounded_channel();
        let event_tx = tx.clone();

        // Spawn background task for event polling
        tokio::spawn(async move {
            let mut last_tick = Instant::now();

            loop {
                // Calculate timeout until next tick
                let timeout = tick_rate.saturating_sub(last_tick.elapsed());

                // Poll for crossterm events with timeout
                if event::poll(timeout).unwrap_or(false) {
                    match event::read() {
                        Ok(CrosstermEvent::Key(key)) => {
                            if event_tx.send(AppEvent::Key(key)).is_err() {
                                // Receiver dropped, exit loop
                                break;
                            }
                        }
                        Ok(CrosstermEvent::Mouse(mouse)) => {
                            if event_tx.send(AppEvent::Mouse(mouse)).is_err() {
                                break;
                            }
                        }
                        Ok(CrosstermEvent::Resize(width, height)) => {
                            if event_tx.send(AppEvent::Resize(width, height)).is_err() {
                                break;
                            }
                        }
                        Ok(CrosstermEvent::FocusGained | CrosstermEvent::FocusLost) => {
                            // Ignore focus events
                        }
                        Ok(CrosstermEvent::Paste(_)) => {
                            // Ignore paste events for now
                        }
                        Err(_) => {
                            // Error reading event, continue polling
                        }
                    }
                }

                // Generate tick event if enough time has passed
                if last_tick.elapsed() >= tick_rate {
                    if event_tx.send(AppEvent::Tick).is_err() {
                        // Receiver dropped, exit loop
                        break;
                    }
                    last_tick = Instant::now();
                }
            }
        });

        Self { rx, tx }
    }

    /// Receives the next event from the event stream.
    ///
    /// This method will wait asynchronously until an event is available.
    ///
    /// # Returns
    ///
    /// The next event, or `None` if the event stream has ended (which shouldn't
    /// happen during normal operation).
    pub async fn next(&mut self) -> Option<AppEvent> {
        self.rx.recv().await
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Test that AppEvent variants can be created and matched.
    #[test]
    fn test_app_event_variants() {
        use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};

        // Key event
        let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
        let event = AppEvent::Key(key);
        assert!(matches!(event, AppEvent::Key(_)));

        // Mouse event
        let mouse = MouseEvent {
            kind: MouseEventKind::Down(MouseButton::Left),
            column: 0,
            row: 0,
            modifiers: KeyModifiers::NONE,
        };
        let event = AppEvent::Mouse(mouse);
        assert!(matches!(event, AppEvent::Mouse(_)));

        // Tick event
        let event = AppEvent::Tick;
        assert!(matches!(event, AppEvent::Tick));

        // Resize event
        let event = AppEvent::Resize(80, 24);
        assert!(matches!(event, AppEvent::Resize(80, 24)));
    }

    /// Test that AppEvent can be cloned.
    #[test]
    fn test_app_event_clone() {
        let event = AppEvent::Tick;
        let cloned = event.clone();
        assert!(matches!(cloned, AppEvent::Tick));

        let event = AppEvent::Resize(100, 50);
        let cloned = event.clone();
        assert!(matches!(cloned, AppEvent::Resize(100, 50)));
    }

    /// Test that EventHandler can be created (doesn't test actual event polling
    /// since that requires a real terminal).
    #[tokio::test]
    async fn test_event_handler_creation() {
        // Just verify the handler can be created without panicking
        let _handler = EventHandler::new(Duration::from_millis(100));
        // Handler will be dropped, which should cleanly stop the background task
    }
}