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}