Skip to main content

gpg_tui/term/
event.rs

1use anyhow::Result;
2use crossterm::event::{self, Event as CrosstermEvent, KeyEvent, MouseEvent};
3use std::sync::mpsc;
4use std::sync::{
5	atomic::{AtomicBool, Ordering},
6	Arc,
7};
8use std::thread;
9use std::time::{Duration, Instant};
10
11/// Representation of terminal events
12/// ([`Crossterm events`] + [`Tick`]).
13///
14/// [`Crossterm events`]: crossterm::event::Event
15/// [`Tick`]: Event::Tick
16#[derive(Clone, Copy, Debug)]
17pub enum Event {
18	/// Key press.
19	Key(KeyEvent),
20	/// Mouse click/scroll.
21	Mouse(MouseEvent),
22	/// Terminal resize.
23	Resize(u16, u16),
24	/// Terminal tick.
25	Tick,
26}
27
28/// Basic event handler for terminal [`events`].
29///
30/// Event types are handled in a common handler thread
31/// and returned to a receiver.
32///
33/// [`events`]: Event
34#[allow(dead_code)]
35#[derive(Debug)]
36pub struct EventHandler {
37	/// Event sender.
38	sender: mpsc::Sender<Event>,
39	/// Event receiver.
40	receiver: mpsc::Receiver<Event>,
41	/// Event handler thread.
42	handler: thread::JoinHandle<()>,
43	/// Is the key input disabled?
44	pub key_input_disabled: Arc<AtomicBool>,
45}
46
47impl EventHandler {
48	/// Constructs a new instance of `EventHandler`.
49	pub fn new(tick_rate: u64) -> Self {
50		let tick_rate = Duration::from_millis(tick_rate);
51		let (sender, receiver) = mpsc::channel();
52		let key_input_disabled = Arc::new(AtomicBool::new(false));
53		let handler = {
54			let sender = sender.clone();
55			let key_input_disabled = key_input_disabled.clone();
56			thread::spawn(move || {
57				let mut last_tick = Instant::now();
58				loop {
59					let timeout = tick_rate
60						.checked_sub(last_tick.elapsed())
61						.unwrap_or(tick_rate);
62					if key_input_disabled.load(Ordering::Relaxed) {
63						thread::sleep(timeout);
64						continue;
65					} else if event::poll(timeout).expect("no events available")
66					{
67						match event::read().expect("unable to read event") {
68							CrosstermEvent::Key(e) => {
69								sender.send(Event::Key(e))
70							}
71							CrosstermEvent::Mouse(e) => {
72								sender.send(Event::Mouse(e))
73							}
74							CrosstermEvent::Resize(w, h) => {
75								sender.send(Event::Resize(w, h))
76							}
77							_ => Ok(()),
78						}
79						.expect("failed to send terminal event")
80					}
81					if last_tick.elapsed() >= tick_rate {
82						sender
83							.send(Event::Tick)
84							.expect("failed to send tick event");
85						last_tick = Instant::now();
86					}
87				}
88			})
89		};
90		Self {
91			sender,
92			receiver,
93			handler,
94			key_input_disabled,
95		}
96	}
97
98	/// Receive the next event from handler.
99	///
100	/// > This function will always block the current thread if
101	/// > there is no data available and it's possible for more data to be sent.
102	///
103	/// (Note that [`Tick`] event is frequently received depending on the tick rate.)
104	///
105	/// [`Tick`]: Event::Tick
106	pub fn next(&self) -> Result<Event, mpsc::RecvError> {
107		self.receiver.recv()
108	}
109}
110
111#[cfg(feature = "tui-tests")]
112#[cfg(test)]
113mod tests {
114	use super::*;
115	use crossterm::event::{KeyCode, KeyModifiers};
116	use pretty_assertions::assert_eq;
117	#[test]
118	fn test_term_event() -> Result<()> {
119		let events = EventHandler::new(100);
120		for step in 0..2 {
121			if step == 1 {
122				let sender = events.sender.clone();
123				thread::spawn(move || {
124					sender.send(Event::Key(KeyEvent::new(
125						KeyCode::Esc,
126						KeyModifiers::NONE,
127					)))
128				});
129			}
130			match events.next()? {
131				Event::Key(key_event) => {
132					if key_event.code == KeyCode::Esc {
133						assert_eq!(1, step);
134						break;
135					}
136				}
137				Event::Tick => assert_eq!(0, step),
138				_ => {}
139			};
140		}
141		Ok(())
142	}
143}