use crossterm::event::{self, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; use std::time::Duration; use tokio::sync::mpsc; use tokio_util::sync::CancellationToken; /// Application events #[derive(Debug, Clone)] pub enum Event { /// Terminal key press event Key(KeyEvent), /// Terminal resize event #[allow(dead_code)] Resize(u16, u16), /// Paste event Paste(String), /// Tick event for regular updates Tick, } /// Event handler that captures terminal events and sends them to the application pub struct EventHandler { sender: mpsc::UnboundedSender, tick_rate: Duration, cancellation_token: CancellationToken, } impl EventHandler { pub fn new( sender: mpsc::UnboundedSender, cancellation_token: CancellationToken, ) -> Self { Self { sender, tick_rate: Duration::from_millis(250), // 4 times per second cancellation_token, } } pub async fn run(&self) { let mut last_tick = tokio::time::Instant::now(); loop { if self.cancellation_token.is_cancelled() { break; } let timeout = self .tick_rate .checked_sub(last_tick.elapsed()) .unwrap_or_else(|| Duration::from_secs(0)); if event::poll(timeout).unwrap_or(false) { match event::read() { Ok(event) => { match event { crossterm::event::Event::Key(key) => { // Only handle KeyEventKind::Press to avoid duplicate events if key.kind == KeyEventKind::Press { let _ = self.sender.send(Event::Key(key)); } } crossterm::event::Event::Resize(width, height) => { let _ = self.sender.send(Event::Resize(width, height)); } crossterm::event::Event::Paste(text) => { let _ = self.sender.send(Event::Paste(text)); } _ => {} } } Err(_) => { // Handle error by continuing the loop continue; } } } if last_tick.elapsed() >= self.tick_rate { let _ = self.sender.send(Event::Tick); last_tick = tokio::time::Instant::now(); } } } } /// Helper functions for key event handling impl Event { /// Check if this is a quit command (Ctrl+C or 'q') pub fn is_quit(&self) -> bool { matches!( self, Event::Key(KeyEvent { code: KeyCode::Char('q'), modifiers: KeyModifiers::NONE, .. }) | Event::Key(KeyEvent { code: KeyCode::Char('c'), modifiers: KeyModifiers::CONTROL, .. }) ) } /// Check if this is an enter key press pub fn is_enter(&self) -> bool { matches!( self, Event::Key(KeyEvent { code: KeyCode::Enter, .. }) ) } /// Check if this is a tab key press #[allow(dead_code)] pub fn is_tab(&self) -> bool { matches!( self, Event::Key(KeyEvent { code: KeyCode::Tab, modifiers: KeyModifiers::NONE, .. }) ) } /// Check if this is a backspace pub fn is_backspace(&self) -> bool { matches!( self, Event::Key(KeyEvent { code: KeyCode::Backspace, .. }) ) } /// Check if this is an escape key press pub fn is_escape(&self) -> bool { matches!( self, Event::Key(KeyEvent { code: KeyCode::Esc, .. }) ) } /// Get the character if this is a character key event pub fn as_char(&self) -> Option { match self { Event::Key(KeyEvent { code: KeyCode::Char(c), modifiers: KeyModifiers::NONE, .. }) => Some(*c), Event::Key(KeyEvent { code: KeyCode::Char(c), modifiers: KeyModifiers::SHIFT, .. }) => Some(*c), _ => None, } } /// Check if this is an up arrow key press pub fn is_up(&self) -> bool { matches!( self, Event::Key(KeyEvent { code: KeyCode::Up, .. }) ) } /// Check if this is a down arrow key press pub fn is_down(&self) -> bool { matches!( self, Event::Key(KeyEvent { code: KeyCode::Down, .. }) ) } /// Check if this is a left arrow key press pub fn is_left(&self) -> bool { matches!( self, Event::Key(KeyEvent { code: KeyCode::Left, .. }) ) } /// Check if this is a right arrow key press pub fn is_right(&self) -> bool { matches!( self, Event::Key(KeyEvent { code: KeyCode::Right, .. }) ) } }