Skip to main content

cleansys_tui/
events.rs

1use std::sync::mpsc;
2use std::thread;
3use std::time::{Duration, Instant};
4
5use anyhow::Result;
6use crossterm::event::{self, Event as CrosstermEvent, KeyEvent};
7
8pub enum Event {
9    Input(KeyEvent),
10    Tick,
11    Resize(u16, u16),
12}
13
14/// A small event handler that wrap crossterm input and tick events
15pub struct Events {
16    /// The event receiver channel
17    rx: mpsc::Receiver<Event>,
18    /// To make sure only one instance of Events exists at a time
19    _tx: mpsc::Sender<Event>,
20}
21
22impl Events {
23    /// Constructs an new instance of `Events` with custom config.
24    pub fn with_config(config: Config) -> Self {
25        let (tx, rx) = mpsc::channel();
26        let event_tx = tx.clone();
27        let tick_rate = config.tick_rate;
28
29        thread::spawn(move || {
30            let mut last_tick = Instant::now();
31            loop {
32                // Poll for events with a timeout matching tick rate
33                let timeout = tick_rate
34                    .checked_sub(last_tick.elapsed())
35                    .unwrap_or_else(|| Duration::from_secs(0));
36
37                match event::poll(timeout) {
38                    Ok(true) => match event::read() {
39                        Ok(CrosstermEvent::Key(key)) => {
40                            if event_tx.send(Event::Input(key)).is_err() {
41                                // Receiver dropped (app is shutting down) —
42                                // exit quietly instead of panicking on the
43                                // next send.
44                                break;
45                            }
46                        }
47                        Ok(CrosstermEvent::Resize(width, height)) => {
48                            if event_tx.send(Event::Resize(width, height)).is_err() {
49                                break;
50                            }
51                        }
52                        Ok(_) => {}
53                        // Terminal event stream error (e.g. stdin closed) —
54                        // nothing sensible to do but stop polling.
55                        Err(_) => break,
56                    },
57                    Ok(false) => {}
58                    // Polling itself failed — same as above, stop cleanly.
59                    Err(_) => break,
60                }
61
62                if last_tick.elapsed() >= tick_rate {
63                    if event_tx.send(Event::Tick).is_err() {
64                        break;
65                    }
66                    last_tick = Instant::now();
67                }
68            }
69        });
70
71        Self { rx, _tx: tx }
72    }
73
74    /// Attempts to read an event.
75    pub fn next(&self) -> Result<Event> {
76        Ok(self.rx.recv()?)
77    }
78}
79
80pub struct Config {
81    pub tick_rate: Duration,
82}
83
84impl Default for Config {
85    fn default() -> Self {
86        Self {
87            tick_rate: Duration::from_millis(250),
88        }
89    }
90}