211 lines
5.6 KiB
Rust
211 lines
5.6 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,
|
|
}
|
|
|
|
/// 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) => {
|
|
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<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,
|
|
..
|
|
})
|
|
)
|
|
}
|
|
}
|