smirrors 0.1.0

Automatic mirror list updater for Linux distributions
Documentation
//! Event handling for the TUI
//!
//! This module manages keyboard input, terminal events, and
//! the event loop for the interactive UI.

use anyhow::Result;
use crossterm::event::{
    self, DisableMouseCapture, EnableMouseCapture, Event as CrosstermEvent,
    KeyCode, KeyEvent, KeyModifiers, MouseEvent,
};
use std::time::Duration;
use tokio::sync::mpsc;

/// Events that can occur in the TUI
#[derive(Debug, Clone)]
pub enum Event {
    /// Keyboard key press
    Key(KeyEvent),

    /// Mouse event
    Mouse(MouseEvent),

    /// Terminal resize event
    Resize(u16, u16),

    /// Tick event for periodic updates
    Tick,
}

/// Supported key actions
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Key {
    Up,
    Down,
    Left,
    Right,
    Enter,
    Esc,
    Backspace,
    Delete,
    Char(char),
    Ctrl(char),
    Alt(char),
    F(u8),
    Tab,
    BackTab,
    Unknown,
}

impl From<KeyEvent> for Key {
    fn from(key_event: KeyEvent) -> Self {
        match key_event.code {
            KeyCode::Up => Key::Up,
            KeyCode::Down => Key::Down,
            KeyCode::Left => Key::Left,
            KeyCode::Right => Key::Right,
            KeyCode::Enter => Key::Enter,
            KeyCode::Esc => Key::Esc,
            KeyCode::Backspace => Key::Backspace,
            KeyCode::Delete => Key::Delete,
            KeyCode::Tab => Key::Tab,
            KeyCode::BackTab => Key::BackTab,
            KeyCode::F(n) => Key::F(n),
            KeyCode::Char(c) => {
                if key_event.modifiers.contains(KeyModifiers::CONTROL) {
                    Key::Ctrl(c)
                } else if key_event.modifiers.contains(KeyModifiers::ALT) {
                    Key::Alt(c)
                } else {
                    Key::Char(c)
                }
            }
            _ => Key::Unknown,
        }
    }
}

/// Event handler for the TUI
pub struct EventHandler {
    /// Sender for events
    tx: mpsc::UnboundedSender<Event>,

    /// Receiver for events
    rx: mpsc::UnboundedReceiver<Event>,

    /// Tick rate in milliseconds
    tick_rate: Duration,
}

impl EventHandler {
    /// Create a new event handler
    ///
    /// # Arguments
    /// * `tick_rate_ms` - Milliseconds between tick events
    ///
    /// # Returns
    /// A new EventHandler instance
    pub fn new(tick_rate_ms: u64) -> Self {
        let tick_rate = Duration::from_millis(tick_rate_ms);
        let (tx, rx) = mpsc::unbounded_channel();

        Self {
            tx,
            rx,
            tick_rate,
        }
    }

    /// Start the event loop
    ///
    /// Spawns a background task that polls for terminal events
    /// and sends them through the channel.
    pub fn start(&self) {
        let tx = self.tx.clone();
        let tick_rate = self.tick_rate;

        tokio::spawn(async move {
            let mut last_tick = tokio::time::Instant::now();

            loop {
                // Calculate timeout until next tick
                let timeout = tick_rate
                    .checked_sub(last_tick.elapsed())
                    .unwrap_or(Duration::from_secs(0));

                // Poll for events with timeout
                if event::poll(timeout).unwrap_or(false) {
                    match event::read() {
                        Ok(CrosstermEvent::Key(key)) => {
                            if tx.send(Event::Key(key)).is_err() {
                                break;
                            }
                        }
                        Ok(CrosstermEvent::Mouse(mouse)) => {
                            if tx.send(Event::Mouse(mouse)).is_err() {
                                break;
                            }
                        }
                        Ok(CrosstermEvent::Resize(w, h)) => {
                            if tx.send(Event::Resize(w, h)).is_err() {
                                break;
                            }
                        }
                        Ok(_) => {}
                        Err(_) => break,
                    }
                }

                // Send tick event
                if last_tick.elapsed() >= tick_rate {
                    if tx.send(Event::Tick).is_err() {
                        break;
                    }
                    last_tick = tokio::time::Instant::now();
                }
            }
        });
    }

    /// Receive the next event (async)
    ///
    /// # Returns
    /// The next event, or None if the channel is closed
    pub async fn next(&mut self) -> Option<Event> {
        self.rx.recv().await
    }

    /// Try to receive an event without blocking
    ///
    /// # Returns
    /// The next event if available, None otherwise
    pub fn try_next(&mut self) -> Option<Event> {
        self.rx.try_recv().ok()
    }
}

/// Enable mouse capture for the terminal
pub fn enable_mouse_capture() -> Result<()> {
    crossterm::execute!(std::io::stdout(), EnableMouseCapture)?;
    Ok(())
}

/// Disable mouse capture for the terminal
pub fn disable_mouse_capture() -> Result<()> {
    crossterm::execute!(std::io::stdout(), DisableMouseCapture)?;
    Ok(())
}

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

    #[test]
    fn test_key_from_event() {
        let key_event = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
        let key: Key = key_event.into();
        assert_eq!(key, Key::Char('a'));

        let key_event = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
        let key: Key = key_event.into();
        assert_eq!(key, Key::Ctrl('c'));

        let key_event = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
        let key: Key = key_event.into();
        assert_eq!(key, Key::Enter);
    }

    #[tokio::test]
    async fn test_event_handler_creation() {
        let handler = EventHandler::new(250);
        assert_eq!(handler.tick_rate, Duration::from_millis(250));
    }
}