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