sim_restaurant/tui/
event.rs

1// Copyright (c) 2026 Graphcore Ltd. All rights reserved.
2
3use std::sync::mpsc;
4use std::thread;
5use std::time::{Duration, Instant};
6
7use crossterm::event::{self, Event as CrosstermEvent, KeyEvent};
8
9pub type AppResult<T> = Result<T, Box<dyn std::error::Error>>;
10
11#[derive(Clone, Copy, Debug)]
12pub enum Event {
13    Tick,
14    Key(KeyEvent),
15    Resize(u16, u16),
16}
17
18#[derive(Debug)]
19pub struct EventHandler {
20    receiver: mpsc::Receiver<Event>,
21    _handler: thread::JoinHandle<()>,
22}
23
24impl EventHandler {
25    #[must_use]
26    pub fn new(tick_rate_ms: u64) -> Self {
27        let tick_rate = Duration::from_millis(tick_rate_ms);
28        let (sender, receiver) = mpsc::channel();
29        let handler = thread::spawn(move || {
30            let mut last_tick = Instant::now();
31            loop {
32                let timeout = tick_rate
33                    .checked_sub(last_tick.elapsed())
34                    .unwrap_or(tick_rate);
35
36                if event::poll(timeout).expect("no events available") {
37                    match event::read().expect("unable to read event") {
38                        CrosstermEvent::Key(key) => sender.send(Event::Key(key)),
39                        CrosstermEvent::Resize(width, height) => {
40                            sender.send(Event::Resize(width, height))
41                        }
42                        _ => continue,
43                    }
44                    .expect("failed to send terminal event");
45                }
46
47                if last_tick.elapsed() >= tick_rate {
48                    sender.send(Event::Tick).expect("failed to send tick event");
49                    last_tick = Instant::now();
50                }
51            }
52        });
53
54        Self {
55            receiver,
56            _handler: handler,
57        }
58    }
59
60    pub fn next(&self) -> AppResult<Event> {
61        Ok(self.receiver.recv()?)
62    }
63}