1pub(crate) mod keymap;
28pub mod theme;
29pub(crate) mod widgets;
30
31use crossterm::event::Event;
32use ratatui::Terminal;
33use std::time::Duration;
34
35pub trait EventSource {
40 fn poll_event(&mut self, timeout: Duration) -> std::io::Result<Option<Event>>;
41}
42
43pub struct CrosstermEventSource {
49 poll_fn: fn(Duration) -> std::io::Result<bool>,
50 read_fn: fn() -> std::io::Result<Event>,
51}
52
53#[allow(clippy::new_without_default)] impl CrosstermEventSource {
55 pub fn new() -> Self {
56 Self {
57 poll_fn: crossterm::event::poll,
58 read_fn: crossterm::event::read,
59 }
60 }
61}
62
63impl EventSource for CrosstermEventSource {
64 fn poll_event(&mut self, timeout: Duration) -> std::io::Result<Option<Event>> {
65 if (self.poll_fn)(timeout)? {
66 Ok(Some((self.read_fn)()?))
67 } else {
68 Ok(None)
69 }
70 }
71}
72
73pub trait TerminalSetup {
78 type B: ratatui::backend::Backend;
79 fn enable(&mut self) -> anyhow::Result<()>;
80 fn create_terminal(&mut self) -> anyhow::Result<Terminal<Self::B>>;
81 fn disable(&mut self);
82 fn print_done(&self);
83}
84
85#[cfg(test)]
88pub(crate) use test_doubles::*;
89
90#[cfg(test)]
91mod test_doubles {
92 use super::*;
93 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
94
95 pub(crate) fn key(code: KeyCode) -> Event {
98 Event::Key(KeyEvent::new(code, KeyModifiers::empty()))
99 }
100
101 pub(crate) fn key_with(code: KeyCode, modifiers: KeyModifiers) -> Event {
103 Event::Key(KeyEvent::new(code, modifiers))
104 }
105
106 pub(crate) struct TestEventSource {
115 events: std::collections::VecDeque<Option<Event>>,
116 fail: bool,
117 }
118
119 impl TestEventSource {
120 pub(crate) fn new(events: Vec<Event>) -> Self {
122 Self {
123 events: events.into_iter().map(Some).collect(),
124 fail: false,
125 }
126 }
127
128 pub(crate) fn new_with_nones(events: Vec<Option<Event>>) -> Self {
131 Self {
132 events: events.into(),
133 fail: false,
134 }
135 }
136
137 pub(crate) fn failing() -> Self {
139 Self {
140 events: std::collections::VecDeque::new(),
141 fail: true,
142 }
143 }
144 }
145
146 impl EventSource for TestEventSource {
147 fn poll_event(&mut self, _timeout: Duration) -> std::io::Result<Option<Event>> {
148 if self.fail {
149 return Err(std::io::Error::other("simulated event source failure"));
150 }
151 Ok(self.events.pop_front().flatten())
152 }
153 }
154
155 pub(crate) struct TestBackendHarness {
160 inner: ratatui::backend::TestBackend,
161 fail_draw: bool,
162 }
163
164 impl TestBackendHarness {
165 pub(crate) fn new(width: u16, height: u16) -> Self {
166 Self {
167 inner: ratatui::backend::TestBackend::new(width, height),
168 fail_draw: false,
169 }
170 }
171
172 pub(crate) fn failing(width: u16, height: u16) -> Self {
173 Self {
174 inner: ratatui::backend::TestBackend::new(width, height),
175 fail_draw: true,
176 }
177 }
178
179 pub(crate) fn buffer(&self) -> &ratatui::buffer::Buffer {
182 self.inner.buffer()
183 }
184
185 pub(crate) fn text(&self) -> String {
187 let buffer = self.buffer();
188 let width = buffer.area.width as usize;
189 buffer
190 .content
191 .chunks(width)
192 .map(|row| row.iter().map(|cell| cell.symbol()).collect::<String>())
193 .collect::<Vec<_>>()
194 .join("\n")
195 }
196 }
197
198 fn into_ok<T>(result: Result<T, std::convert::Infallible>) -> std::io::Result<T> {
204 match result {
205 Ok(value) => Ok(value),
206 }
207 }
208
209 impl ratatui::backend::Backend for TestBackendHarness {
210 type Error = std::io::Error;
211
212 fn draw<'a, I>(&mut self, content: I) -> std::io::Result<()>
213 where
214 I: Iterator<Item = (u16, u16, &'a ratatui::buffer::Cell)>,
215 {
216 if self.fail_draw {
217 return Err(std::io::Error::other("simulated draw failure"));
218 }
219 into_ok(self.inner.draw(content))
220 }
221
222 fn hide_cursor(&mut self) -> std::io::Result<()> {
223 into_ok(self.inner.hide_cursor())
224 }
225 fn show_cursor(&mut self) -> std::io::Result<()> {
226 into_ok(self.inner.show_cursor())
227 }
228 fn get_cursor_position(&mut self) -> std::io::Result<ratatui::layout::Position> {
229 into_ok(self.inner.get_cursor_position())
230 }
231 fn set_cursor_position<P: Into<ratatui::layout::Position>>(
232 &mut self,
233 position: P,
234 ) -> std::io::Result<()> {
235 into_ok(self.inner.set_cursor_position(position))
236 }
237 fn clear(&mut self) -> std::io::Result<()> {
238 into_ok(self.inner.clear())
239 }
240 fn clear_region(&mut self, region: ratatui::backend::ClearType) -> std::io::Result<()> {
241 into_ok(self.inner.clear_region(region))
242 }
243 fn size(&self) -> std::io::Result<ratatui::layout::Size> {
244 into_ok(self.inner.size())
245 }
246 fn window_size(&mut self) -> std::io::Result<ratatui::backend::WindowSize> {
247 into_ok(self.inner.window_size())
248 }
249 fn flush(&mut self) -> std::io::Result<()> {
250 into_ok(self.inner.flush())
251 }
252 }
253
254 pub(crate) fn test_terminal() -> Terminal<TestBackendHarness> {
256 Terminal::new(TestBackendHarness::new(120, 40)).unwrap()
257 }
258
259 pub(crate) struct TestSetup {
266 pub(crate) enable_should_fail: bool,
267 pub(crate) create_should_fail: bool,
268 }
269
270 impl TestSetup {
271 pub(crate) fn new() -> Self {
272 Self {
273 enable_should_fail: false,
274 create_should_fail: false,
275 }
276 }
277 }
278
279 impl TerminalSetup for TestSetup {
280 type B = TestBackendHarness;
281
282 fn enable(&mut self) -> anyhow::Result<()> {
283 if self.enable_should_fail {
284 anyhow::bail!("simulated enable failure");
285 }
286 Ok(())
287 }
288
289 fn create_terminal(&mut self) -> anyhow::Result<Terminal<Self::B>> {
290 if self.create_should_fail {
291 anyhow::bail!("simulated create_terminal failure");
292 }
293 Terminal::new(TestBackendHarness::new(80, 24)).map_err(anyhow::Error::from)
294 }
295
296 fn disable(&mut self) {}
297
298 fn print_done(&self) {}
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305 use crossterm::event::KeyCode;
306
307 fn poll_ready(_: Duration) -> std::io::Result<bool> {
316 Ok(true)
317 }
318 fn poll_timeout(_: Duration) -> std::io::Result<bool> {
319 Ok(false)
320 }
321 fn poll_fails(_: Duration) -> std::io::Result<bool> {
322 Err(std::io::Error::other("poll exploded"))
323 }
324 fn read_resize() -> std::io::Result<Event> {
325 Ok(Event::Resize(80, 24))
326 }
327 fn read_fails() -> std::io::Result<Event> {
328 Err(std::io::Error::other("read exploded"))
329 }
330
331 #[test]
332 fn crossterm_event_source_returns_the_read_event_when_poll_reports_ready() {
333 let mut source = CrosstermEventSource {
334 poll_fn: poll_ready,
335 read_fn: read_resize,
336 };
337
338 let event = source.poll_event(Duration::from_millis(1)).unwrap();
339
340 assert_eq!(event, Some(Event::Resize(80, 24)));
341 }
342
343 #[test]
344 fn crossterm_event_source_returns_none_when_poll_times_out() {
345 let mut source = CrosstermEventSource {
348 poll_fn: poll_timeout,
349 read_fn: read_resize,
350 };
351
352 let event = source.poll_event(Duration::from_millis(1)).unwrap();
353
354 assert!(event.is_none());
355 }
356
357 #[test]
358 fn crossterm_event_source_propagates_a_poll_error() {
359 let mut source = CrosstermEventSource {
360 poll_fn: poll_fails,
361 read_fn: read_resize,
362 };
363
364 let err = source.poll_event(Duration::from_millis(1)).unwrap_err();
365
366 assert!(err.to_string().contains("poll exploded"));
367 }
368
369 #[test]
370 fn crossterm_event_source_propagates_a_read_error() {
371 let mut source = CrosstermEventSource {
372 poll_fn: poll_ready,
373 read_fn: read_fails,
374 };
375
376 let err = source.poll_event(Duration::from_millis(1)).unwrap_err();
377
378 assert!(err.to_string().contains("read exploded"));
379 }
380
381 #[test]
382 fn crossterm_event_source_new_stores_the_real_crossterm_functions() {
383 let _source = CrosstermEventSource::new();
386 }
387
388 #[test]
389 fn test_event_source_yields_scripted_events_then_none_forever() {
390 let mut source = TestEventSource::new(vec![key(KeyCode::Esc)]);
391
392 assert_eq!(
393 source.poll_event(Duration::from_millis(1)).unwrap(),
394 Some(key(KeyCode::Esc))
395 );
396 assert!(
398 source
399 .poll_event(Duration::from_millis(1))
400 .unwrap()
401 .is_none()
402 );
403 assert!(
404 source
405 .poll_event(Duration::from_millis(1))
406 .unwrap()
407 .is_none()
408 );
409 }
410
411 #[test]
412 fn test_event_source_interleaves_explicit_timeout_ticks() {
413 let mut source = TestEventSource::new_with_nones(vec![None, Some(key(KeyCode::Enter))]);
414
415 assert!(
416 source
417 .poll_event(Duration::from_millis(1))
418 .unwrap()
419 .is_none()
420 );
421 assert_eq!(
422 source.poll_event(Duration::from_millis(1)).unwrap(),
423 Some(key(KeyCode::Enter))
424 );
425 }
426
427 #[test]
428 fn test_event_source_failing_mode_errors_on_every_poll() {
429 let mut source = TestEventSource::failing();
430
431 assert!(source.poll_event(Duration::from_millis(1)).is_err());
432 assert!(source.poll_event(Duration::from_millis(1)).is_err());
433 }
434
435 #[test]
436 fn key_with_carries_its_modifiers() {
437 let event = key_with(KeyCode::Char('s'), crossterm::event::KeyModifiers::CONTROL);
438
439 assert_eq!(
440 event,
441 Event::Key(crossterm::event::KeyEvent::new(
442 KeyCode::Char('s'),
443 crossterm::event::KeyModifiers::CONTROL
444 ))
445 );
446 assert_ne!(event, key(KeyCode::Char('s')));
448 }
449
450 #[test]
451 fn test_backend_harness_draws_or_fails_on_demand() {
452 use ratatui::backend::Backend;
453
454 let mut ok = TestBackendHarness::new(10, 3);
455 assert!(ok.draw(std::iter::empty()).is_ok());
456 assert!(ok.hide_cursor().is_ok());
458 assert!(ok.show_cursor().is_ok());
459 assert!(ok.get_cursor_position().is_ok());
460 assert!(
461 ok.set_cursor_position(ratatui::layout::Position::new(0, 0))
462 .is_ok()
463 );
464 assert!(ok.clear().is_ok());
465 assert!(ok.clear_region(ratatui::backend::ClearType::All).is_ok());
466 assert!(ok.size().is_ok());
467 assert!(ok.window_size().is_ok());
468 assert!(ok.flush().is_ok());
469
470 let mut bad = TestBackendHarness::failing(10, 3);
471 assert!(bad.draw(std::iter::empty()).is_err());
472 }
473
474 #[test]
475 fn test_terminal_is_ready_to_draw() {
476 let mut terminal = test_terminal();
477 assert!(terminal.draw(|_| {}).is_ok());
478 }
479
480 #[test]
481 fn test_setup_succeeds_by_default_and_fails_when_switched() {
482 let mut setup = TestSetup::new();
483 assert!(setup.enable().is_ok());
484 assert!(setup.create_terminal().is_ok());
485 setup.disable();
486 setup.print_done();
487
488 let mut enable_fails = TestSetup {
489 enable_should_fail: true,
490 create_should_fail: false,
491 };
492 assert!(enable_fails.enable().is_err());
493
494 let mut create_fails = TestSetup {
495 enable_should_fail: false,
496 create_should_fail: true,
497 };
498 assert!(create_fails.create_terminal().is_err());
499 }
500}