kimun_notes/app/
events.rs1use std::io;
17use std::pin::Pin;
18
19use crossterm::event::{Event as CrosstermEvent, EventStream, KeyEventKind};
20use futures::{Stream, StreamExt};
21use tokio::sync::mpsc;
22use tokio::sync::mpsc::error::TryRecvError;
23
24use crate::app::ctrl_h::CtrlHPolicy;
25use crate::components::events::{AppEvent, AppTx, InputEvent};
26
27pub type InputSource = Pin<Box<dyn Stream<Item = AppEvent> + Send>>;
31
32pub struct EventHandler {
35 tx: AppTx,
36 rx: mpsc::UnboundedReceiver<AppEvent>,
37 input: InputSource,
38}
39
40impl EventHandler {
41 pub fn new(ctrl_h: CtrlHPolicy) -> Self {
46 Self::from_input(crossterm_input(ctrl_h))
47 }
48
49 pub fn from_input(input: impl Stream<Item = AppEvent> + Send + 'static) -> Self {
53 let (tx, rx) = mpsc::unbounded_channel();
54 Self {
55 tx,
56 rx,
57 input: Box::pin(input.fuse()),
58 }
59 }
60
61 pub fn app_sender(&self) -> AppTx {
63 self.tx.clone()
64 }
65
66 pub fn try_next(&mut self) -> Option<AppEvent> {
74 match self.rx.try_recv() {
75 Ok(msg) => Some(msg),
76 Err(TryRecvError::Empty) => None,
77 Err(TryRecvError::Disconnected) => {
78 unreachable!(
79 "EventHandler::tx is owned by this struct and the `&mut self` borrow \
80 guarantees it outlives this call; channel cannot be Disconnected here"
81 )
82 }
83 }
84 }
85
86 pub async fn next(&mut self) -> AppEvent {
89 tokio::select! {
90 biased;
91 Some(msg) = self.rx.recv() => msg,
92 event = self.input.next() => event.unwrap_or(AppEvent::Quit),
93 }
94 }
95}
96
97fn crossterm_input(ctrl_h: CtrlHPolicy) -> impl Stream<Item = AppEvent> + Send {
99 EventStream::new().filter_map(move |event| {
100 tracing::debug!("RAW EVENT: {:?}", event);
101 futures::future::ready(decode(event, ctrl_h))
102 })
103}
104
105pub(crate) fn decode(event: io::Result<CrosstermEvent>, ctrl_h: CtrlHPolicy) -> Option<AppEvent> {
115 match event {
116 Ok(CrosstermEvent::Key(key)) if key.kind != KeyEventKind::Release => {
117 Some(AppEvent::Input(InputEvent::Key(ctrl_h.apply(key))))
118 }
119 Ok(CrosstermEvent::Mouse(mouse)) => Some(AppEvent::Input(InputEvent::Mouse(mouse))),
120 Ok(CrosstermEvent::Paste(text)) => Some(AppEvent::Input(InputEvent::Paste(text))),
121 Ok(CrosstermEvent::Resize(_, _)) => Some(AppEvent::Redraw),
122 Ok(_) => None,
123 Err(e) => {
124 tracing::warn!("terminal input error: {e}");
125 None
126 }
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use futures::stream;
134 use ratatui::crossterm::event::{
135 Event as CrosstermEvent, KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers,
136 };
137
138 fn key(code: KeyCode, kind: KeyEventKind) -> CrosstermEvent {
139 CrosstermEvent::Key(KeyEvent {
140 code,
141 modifiers: KeyModifiers::NONE,
142 kind,
143 state: KeyEventState::NONE,
144 })
145 }
146
147 const CHORD: CtrlHPolicy = CtrlHPolicy::Chord;
149
150 #[test]
151 fn decode_keeps_presses_and_drops_releases() {
152 assert!(matches!(
153 decode(Ok(key(KeyCode::Char('a'), KeyEventKind::Press)), CHORD),
154 Some(AppEvent::Input(InputEvent::Key(k))) if k.code == KeyCode::Char('a')
155 ));
156 assert!(decode(Ok(key(KeyCode::Char('a'), KeyEventKind::Release)), CHORD).is_none());
157 }
158
159 #[test]
160 fn decode_turns_a_resize_into_a_redraw_and_skips_errors() {
161 assert!(matches!(
162 decode(Ok(CrosstermEvent::Resize(80, 24)), CHORD),
163 Some(AppEvent::Redraw)
164 ));
165 assert!(decode(Err(std::io::Error::other("hangup")), CHORD).is_none());
166 }
167
168 #[test]
173 fn decode_applies_the_ctrl_h_rule() {
174 let ctrl_h = CrosstermEvent::Key(KeyEvent::new(KeyCode::Char('h'), KeyModifiers::CONTROL));
175 assert!(matches!(
176 decode(Ok(ctrl_h.clone()), CtrlHPolicy::Backspace),
177 Some(AppEvent::Input(InputEvent::Key(k)))
178 if k.code == KeyCode::Backspace && k.modifiers == KeyModifiers::NONE
179 ));
180 assert!(matches!(
181 decode(Ok(ctrl_h), CHORD),
182 Some(AppEvent::Input(InputEvent::Key(k)))
183 if k.code == KeyCode::Char('h') && k.modifiers == KeyModifiers::CONTROL
184 ));
185 }
186
187 #[test]
190 fn the_ctrl_h_rule_does_not_revive_releases() {
191 let release = CrosstermEvent::Key(KeyEvent::new_with_kind(
192 KeyCode::Char('h'),
193 KeyModifiers::CONTROL,
194 KeyEventKind::Release,
195 ));
196 assert!(decode(Ok(release), CtrlHPolicy::Backspace).is_none());
197 }
198
199 #[tokio::test]
200 async fn a_scripted_input_source_is_delivered_in_order() {
201 let mut events = EventHandler::from_input(stream::iter([
202 AppEvent::Redraw,
203 AppEvent::Input(InputEvent::Paste("p".into())),
204 ]));
205 assert!(matches!(events.next().await, AppEvent::Redraw));
206 assert!(matches!(
207 events.next().await,
208 AppEvent::Input(InputEvent::Paste(s)) if s == "p"
209 ));
210 }
211
212 #[tokio::test]
213 async fn app_messages_are_drained_before_input() {
214 let mut events = EventHandler::from_input(stream::iter([AppEvent::Redraw]));
215 events.app_sender().send(AppEvent::Quit).unwrap();
216 assert!(matches!(events.next().await, AppEvent::Quit));
218 assert!(matches!(events.next().await, AppEvent::Redraw));
219 }
220
221 #[tokio::test]
222 async fn an_exhausted_input_source_yields_quit() {
223 let mut events = EventHandler::from_input(stream::empty());
224 assert!(matches!(events.next().await, AppEvent::Quit));
225 assert!(matches!(events.next().await, AppEvent::Quit));
227 }
228
229 #[test]
230 fn try_next_only_peeks_the_app_channel() {
231 let mut events = EventHandler::from_input(stream::iter([AppEvent::Redraw]));
232 assert!(events.try_next().is_none());
233 events.app_sender().send(AppEvent::Redraw).unwrap();
234 assert!(matches!(events.try_next(), Some(AppEvent::Redraw)));
235 }
236}