use crossterm::event::{self, Event, KeyEvent, KeyEventKind};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{self, Receiver, Sender, TryRecvError};
use std::sync::Arc;
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
const INPUT_POLL_MS: u64 = 50;
pub struct InputHandler {
rx: Receiver<TimestampedKey>,
shutdown: Arc<AtomicBool>,
thread_handle: Option<JoinHandle<()>>,
}
#[derive(Debug, Clone)]
pub struct TimestampedKey {
pub event: KeyEvent,
pub timestamp: Instant,
}
impl InputHandler {
pub fn spawn() -> Self {
let (tx, rx): (Sender<TimestampedKey>, Receiver<TimestampedKey>) = mpsc::channel();
let shutdown = Arc::new(AtomicBool::new(false));
let shutdown_clone = Arc::clone(&shutdown);
let thread_handle = thread::Builder::new()
.name("ptop-input".to_string())
.spawn(move || {
Self::input_loop(tx, shutdown_clone);
})
.expect("Failed to spawn input thread");
Self {
rx,
shutdown,
thread_handle: Some(thread_handle),
}
}
fn input_loop(tx: Sender<TimestampedKey>, shutdown: Arc<AtomicBool>) {
let poll_duration = Duration::from_millis(INPUT_POLL_MS);
loop {
if shutdown.load(Ordering::Relaxed) {
break;
}
match event::poll(poll_duration) {
Ok(true) => {
if let Ok(Event::Key(key)) = event::read() {
if key.kind == KeyEventKind::Press {
let timestamped = TimestampedKey {
event: key,
timestamp: Instant::now(),
};
if tx.send(timestamped).is_err() {
break;
}
}
}
}
Ok(false) => {
}
Err(_) => {
break;
}
}
}
}
pub fn try_recv(&self) -> Option<TimestampedKey> {
match self.rx.try_recv() {
Ok(key) => Some(key),
Err(TryRecvError::Empty) => None,
Err(TryRecvError::Disconnected) => None,
}
}
pub fn drain(&self) -> Vec<TimestampedKey> {
std::iter::from_fn(|| self.try_recv()).collect()
}
pub fn has_pending(&self) -> bool {
false
}
pub fn latency(event: &TimestampedKey) -> Duration {
event.timestamp.elapsed()
}
pub fn shutdown(&self) {
self.shutdown.store(true, Ordering::Relaxed);
}
}
impl Drop for InputHandler {
fn drop(&mut self) {
self.shutdown.store(true, Ordering::Relaxed);
if let Some(handle) = self.thread_handle.take() {
let start = Instant::now();
while !handle.is_finished() && start.elapsed() < Duration::from_millis(100) {
thread::sleep(Duration::from_millis(5));
}
let _ = handle.join();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::{KeyCode, KeyModifiers};
#[test]
fn test_f_input_004_graceful_shutdown() {
let start = Instant::now();
{
let handler = InputHandler::spawn();
thread::sleep(Duration::from_millis(10));
drop(handler);
}
let elapsed = start.elapsed();
assert!(
elapsed < Duration::from_millis(200),
"Shutdown took {:?}, expected < 200ms",
elapsed
);
}
#[test]
fn test_f_input_001_latency_measurement() {
let event = TimestampedKey {
event: KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE),
timestamp: Instant::now(),
};
thread::sleep(Duration::from_millis(10));
let latency = InputHandler::latency(&event);
assert!(
latency >= Duration::from_millis(10),
"Latency {:?} should be >= 10ms",
latency
);
assert!(
latency < Duration::from_millis(100),
"Latency {:?} should be < 100ms",
latency
);
}
#[test]
fn test_drain_empty() {
let handler = InputHandler::spawn();
thread::sleep(Duration::from_millis(20));
let events = handler.drain();
assert!(events.is_empty(), "Should have no events");
}
#[test]
fn test_try_recv_empty() {
let handler = InputHandler::spawn();
thread::sleep(Duration::from_millis(20));
assert!(
handler.try_recv().is_none(),
"Should return None when no events"
);
}
#[test]
fn test_f_input_002_channel_ordering() {
let (tx, rx): (Sender<TimestampedKey>, Receiver<TimestampedKey>) = mpsc::channel();
for i in 0..100u8 {
let key = TimestampedKey {
event: KeyEvent::new(
KeyCode::Char(char::from(b'a' + (i % 26))),
KeyModifiers::NONE,
),
timestamp: Instant::now(),
};
tx.send(key).expect("Channel should accept event");
}
let mut count = 0;
while let Ok(_key) = rx.try_recv() {
count += 1;
}
assert_eq!(count, 100, "All 100 events should be received, got {count}");
}
#[test]
fn test_f_input_003_thread_isolation() {
let handler = InputHandler::spawn();
thread::sleep(Duration::from_millis(20));
let render_start = Instant::now();
thread::sleep(Duration::from_millis(100)); let render_time = render_start.elapsed();
assert!(
render_time >= Duration::from_millis(95),
"Render simulation should take ~100ms, took {:?}",
render_time
);
let events = handler.drain();
assert!(
events.is_empty(),
"No events expected (no keyboard input in test)"
);
drop(handler);
}
#[test]
fn test_multiple_handlers_no_leak() {
for i in 0..5 {
let handler = InputHandler::spawn();
thread::sleep(Duration::from_millis(10));
drop(handler);
thread::sleep(Duration::from_millis(20));
assert!(true, "Handler {i} created and dropped successfully");
}
}
#[test]
fn test_shutdown_signal() {
let handler = InputHandler::spawn();
thread::sleep(Duration::from_millis(20));
handler.shutdown();
thread::sleep(Duration::from_millis(100));
let drop_start = Instant::now();
drop(handler);
let drop_time = drop_start.elapsed();
assert!(
drop_time < Duration::from_millis(50),
"Drop after shutdown should be fast, took {:?}",
drop_time
);
}
#[test]
fn test_has_pending_always_false() {
let handler = InputHandler::spawn();
thread::sleep(Duration::from_millis(20));
assert!(!handler.has_pending());
drop(handler);
}
#[test]
fn test_timestamped_key_clone() {
let original = TimestampedKey {
event: KeyEvent::new(KeyCode::Char('x'), KeyModifiers::CONTROL),
timestamp: Instant::now(),
};
let cloned = original.clone();
assert_eq!(cloned.event.code, original.event.code);
assert_eq!(cloned.event.modifiers, original.event.modifiers);
assert_eq!(cloned.timestamp, original.timestamp);
}
#[test]
fn test_timestamped_key_debug() {
let key = TimestampedKey {
event: KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE),
timestamp: Instant::now(),
};
let debug = format!("{:?}", key);
assert!(debug.contains("TimestampedKey"));
assert!(debug.contains("event"));
assert!(debug.contains("timestamp"));
}
#[test]
fn test_try_recv_disconnected() {
let (tx, rx): (Sender<TimestampedKey>, Receiver<TimestampedKey>) = mpsc::channel();
drop(tx);
match rx.try_recv() {
Err(TryRecvError::Disconnected) => {
assert!(true);
}
_ => panic!("Expected Disconnected error"),
}
}
#[test]
fn test_rapid_spawn_and_shutdown() {
for _ in 0..3 {
let handler = InputHandler::spawn();
handler.shutdown();
thread::sleep(Duration::from_millis(60));
drop(handler);
}
}
#[test]
fn test_input_poll_constant() {
assert_eq!(INPUT_POLL_MS, 50, "Poll interval should be 50ms");
}
}