Add App core struct with event-handling and initialization logic for TUI.
This commit is contained in:
205
crates/owlen-tui/src/events.rs
Normal file
205
crates/owlen-tui/src/events.rs
Normal file
@@ -0,0 +1,205 @@
|
||||
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),
|
||||
/// 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));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
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,
|
||||
..
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user