Files
owlen/crates/owlen-tui/src/events.rs

215 lines
5.5 KiB
Rust

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,
}
/// Convert a raw crossterm event into an application event.
pub fn from_crossterm_event(raw: crossterm::event::Event) -> Option<Event> {
match raw {
crossterm::event::Event::Key(key) => {
if key.kind == KeyEventKind::Press {
Some(Event::Key(key))
} else {
None
}
}
crossterm::event::Event::Resize(width, height) => Some(Event::Resize(width, height)),
crossterm::event::Event::Paste(text) => Some(Event::Paste(text)),
_ => None,
}
}
/// Event handler that captures terminal events and sends them to the application
pub struct EventHandler {
sender: mpsc::UnboundedSender<Event>,
tick_rate: Duration,
cancellation_token: CancellationToken,
}
impl EventHandler {
pub fn new(
sender: mpsc::UnboundedSender<Event>,
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) => {
if let Some(converted) = from_crossterm_event(event) {
let _ = self.sender.send(converted);
}
}
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<char> {
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,
..
})
)
}
}