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>>;
43}
44
45pub struct CrosstermEventSource {
51 poll_fn: fn(Duration) -> std::io::Result<bool>,
52 read_fn: fn() -> std::io::Result<Event>,
53}
54
55impl CrosstermEventSource {
56 pub fn open() -> Self {
63 Self {
64 poll_fn: crossterm::event::poll,
65 read_fn: crossterm::event::read,
66 }
67 }
68}
69
70impl EventSource for CrosstermEventSource {
71 fn poll_event(&mut self, timeout: Duration) -> std::io::Result<Option<Event>> {
72 if (self.poll_fn)(timeout)? {
73 Ok(Some((self.read_fn)()?))
74 } else {
75 Ok(None)
76 }
77 }
78}
79
80pub trait TerminalSetup {
85 type B: ratatui::backend::Backend;
88 fn enable(&mut self) -> anyhow::Result<()>;
90 fn create_terminal(&mut self) -> anyhow::Result<Terminal<Self::B>>;
92 fn disable(&mut self);
96 fn print_done(&self);
98}
99
100#[cfg(test)]
103pub(crate) use test_doubles::*;
104
105#[cfg(test)]
106mod test_doubles {
107 use super::*;
108 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
109
110 pub(crate) fn key(code: KeyCode) -> Event {
113 Event::Key(KeyEvent::new(code, KeyModifiers::empty()))
114 }
115
116 pub(crate) fn key_with(code: KeyCode, modifiers: KeyModifiers) -> Event {
118 Event::Key(KeyEvent::new(code, modifiers))
119 }
120
121 pub(crate) struct TestEventSource {
130 events: std::collections::VecDeque<Option<Event>>,
131 fail: bool,
132 }
133
134 impl TestEventSource {
135 pub(crate) fn new(events: Vec<Event>) -> Self {
137 Self {
138 events: events.into_iter().map(Some).collect(),
139 fail: false,
140 }
141 }
142
143 pub(crate) fn new_with_nones(events: Vec<Option<Event>>) -> Self {
146 Self {
147 events: events.into(),
148 fail: false,
149 }
150 }
151
152 pub(crate) fn failing() -> Self {
154 Self {
155 events: std::collections::VecDeque::new(),
156 fail: true,
157 }
158 }
159 }
160
161 impl EventSource for TestEventSource {
162 fn poll_event(&mut self, _timeout: Duration) -> std::io::Result<Option<Event>> {
163 if self.fail {
164 return Err(std::io::Error::other("simulated event source failure"));
165 }
166 Ok(self.events.pop_front().flatten())
167 }
168 }
169
170 pub(crate) struct TestBackendHarness {
175 inner: ratatui::backend::TestBackend,
176 fail_draw: bool,
177 }
178
179 impl TestBackendHarness {
180 pub(crate) fn new(width: u16, height: u16) -> Self {
181 Self {
182 inner: ratatui::backend::TestBackend::new(width, height),
183 fail_draw: false,
184 }
185 }
186
187 pub(crate) fn failing(width: u16, height: u16) -> Self {
188 Self {
189 inner: ratatui::backend::TestBackend::new(width, height),
190 fail_draw: true,
191 }
192 }
193
194 pub(crate) fn buffer(&self) -> &ratatui::buffer::Buffer {
197 self.inner.buffer()
198 }
199
200 pub(crate) fn text(&self) -> String {
202 let buffer = self.buffer();
203 let width = buffer.area.width as usize;
204 buffer
205 .content
206 .chunks(width)
207 .map(|row| row.iter().map(|cell| cell.symbol()).collect::<String>())
208 .collect::<Vec<_>>()
209 .join("\n")
210 }
211 }
212
213 fn into_ok<T>(result: Result<T, std::convert::Infallible>) -> std::io::Result<T> {
219 match result {
220 Ok(value) => Ok(value),
221 }
222 }
223
224 impl ratatui::backend::Backend for TestBackendHarness {
225 type Error = std::io::Error;
226
227 fn draw<'a, I>(&mut self, content: I) -> std::io::Result<()>
228 where
229 I: Iterator<Item = (u16, u16, &'a ratatui::buffer::Cell)>,
230 {
231 if self.fail_draw {
232 return Err(std::io::Error::other("simulated draw failure"));
233 }
234 into_ok(self.inner.draw(content))
235 }
236
237 fn hide_cursor(&mut self) -> std::io::Result<()> {
238 into_ok(self.inner.hide_cursor())
239 }
240 fn show_cursor(&mut self) -> std::io::Result<()> {
241 into_ok(self.inner.show_cursor())
242 }
243 fn get_cursor_position(&mut self) -> std::io::Result<ratatui::layout::Position> {
244 into_ok(self.inner.get_cursor_position())
245 }
246 fn set_cursor_position<P: Into<ratatui::layout::Position>>(
247 &mut self,
248 position: P,
249 ) -> std::io::Result<()> {
250 into_ok(self.inner.set_cursor_position(position))
251 }
252 fn clear(&mut self) -> std::io::Result<()> {
253 into_ok(self.inner.clear())
254 }
255 fn clear_region(&mut self, region: ratatui::backend::ClearType) -> std::io::Result<()> {
256 into_ok(self.inner.clear_region(region))
257 }
258 fn size(&self) -> std::io::Result<ratatui::layout::Size> {
259 into_ok(self.inner.size())
260 }
261 fn window_size(&mut self) -> std::io::Result<ratatui::backend::WindowSize> {
262 into_ok(self.inner.window_size())
263 }
264 fn flush(&mut self) -> std::io::Result<()> {
265 into_ok(self.inner.flush())
266 }
267 }
268
269 pub(crate) fn test_terminal() -> Terminal<TestBackendHarness> {
271 Terminal::new(TestBackendHarness::new(120, 40)).unwrap()
272 }
273
274 pub(crate) struct TestSetup {
281 pub(crate) enable_should_fail: bool,
282 pub(crate) create_should_fail: bool,
283 pub(crate) draw_should_fail: bool,
286 }
287
288 impl TestSetup {
289 pub(crate) fn new() -> Self {
290 Self {
291 enable_should_fail: false,
292 create_should_fail: false,
293 draw_should_fail: false,
294 }
295 }
296 }
297
298 impl TerminalSetup for TestSetup {
299 type B = TestBackendHarness;
300
301 fn enable(&mut self) -> anyhow::Result<()> {
302 if self.enable_should_fail {
303 anyhow::bail!("simulated enable failure");
304 }
305 Ok(())
306 }
307
308 fn create_terminal(&mut self) -> anyhow::Result<Terminal<Self::B>> {
309 if self.create_should_fail {
310 anyhow::bail!("simulated create_terminal failure");
311 }
312 let backend = match self.draw_should_fail {
313 true => TestBackendHarness::failing(80, 24),
314 false => TestBackendHarness::new(80, 24),
315 };
316 Terminal::new(backend).map_err(anyhow::Error::from)
317 }
318
319 fn disable(&mut self) {}
320
321 fn print_done(&self) {}
322 }
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328 use crossterm::event::KeyCode;
329
330 fn poll_ready(_: Duration) -> std::io::Result<bool> {
339 Ok(true)
340 }
341 fn poll_timeout(_: Duration) -> std::io::Result<bool> {
342 Ok(false)
343 }
344 fn poll_fails(_: Duration) -> std::io::Result<bool> {
345 Err(std::io::Error::other("poll exploded"))
346 }
347 fn read_resize() -> std::io::Result<Event> {
348 Ok(Event::Resize(80, 24))
349 }
350 fn read_fails() -> std::io::Result<Event> {
351 Err(std::io::Error::other("read exploded"))
352 }
353
354 #[test]
355 fn crossterm_event_source_returns_the_read_event_when_poll_reports_ready() {
356 let mut source = CrosstermEventSource {
357 poll_fn: poll_ready,
358 read_fn: read_resize,
359 };
360
361 let event = source.poll_event(Duration::from_millis(1)).unwrap();
362
363 assert_eq!(event, Some(Event::Resize(80, 24)));
364 }
365
366 #[test]
367 fn crossterm_event_source_returns_none_when_poll_times_out() {
368 let mut source = CrosstermEventSource {
371 poll_fn: poll_timeout,
372 read_fn: read_resize,
373 };
374
375 let event = source.poll_event(Duration::from_millis(1)).unwrap();
376
377 assert!(event.is_none());
378 }
379
380 #[test]
381 fn crossterm_event_source_propagates_a_poll_error() {
382 let mut source = CrosstermEventSource {
383 poll_fn: poll_fails,
384 read_fn: read_resize,
385 };
386
387 let err = source.poll_event(Duration::from_millis(1)).unwrap_err();
388
389 assert!(err.to_string().contains("poll exploded"));
390 }
391
392 #[test]
393 fn crossterm_event_source_propagates_a_read_error() {
394 let mut source = CrosstermEventSource {
395 poll_fn: poll_ready,
396 read_fn: read_fails,
397 };
398
399 let err = source.poll_event(Duration::from_millis(1)).unwrap_err();
400
401 assert!(err.to_string().contains("read exploded"));
402 }
403
404 #[test]
405 fn crossterm_event_source_new_stores_the_real_crossterm_functions() {
406 let _source = CrosstermEventSource::open();
409 }
410
411 #[test]
412 fn test_event_source_yields_scripted_events_then_none_forever() {
413 let mut source = TestEventSource::new(vec![key(KeyCode::Esc)]);
414
415 assert_eq!(
416 source.poll_event(Duration::from_millis(1)).unwrap(),
417 Some(key(KeyCode::Esc))
418 );
419 assert!(
421 source
422 .poll_event(Duration::from_millis(1))
423 .unwrap()
424 .is_none()
425 );
426 assert!(
427 source
428 .poll_event(Duration::from_millis(1))
429 .unwrap()
430 .is_none()
431 );
432 }
433
434 #[test]
435 fn test_event_source_interleaves_explicit_timeout_ticks() {
436 let mut source = TestEventSource::new_with_nones(vec![None, Some(key(KeyCode::Enter))]);
437
438 assert!(
439 source
440 .poll_event(Duration::from_millis(1))
441 .unwrap()
442 .is_none()
443 );
444 assert_eq!(
445 source.poll_event(Duration::from_millis(1)).unwrap(),
446 Some(key(KeyCode::Enter))
447 );
448 }
449
450 #[test]
451 fn test_event_source_failing_mode_errors_on_every_poll() {
452 let mut source = TestEventSource::failing();
453
454 assert!(source.poll_event(Duration::from_millis(1)).is_err());
455 assert!(source.poll_event(Duration::from_millis(1)).is_err());
456 }
457
458 #[test]
459 fn key_with_carries_its_modifiers() {
460 let event = key_with(KeyCode::Char('s'), crossterm::event::KeyModifiers::CONTROL);
461
462 assert_eq!(
463 event,
464 Event::Key(crossterm::event::KeyEvent::new(
465 KeyCode::Char('s'),
466 crossterm::event::KeyModifiers::CONTROL
467 ))
468 );
469 assert_ne!(event, key(KeyCode::Char('s')));
471 }
472
473 #[test]
474 fn test_backend_harness_draws_or_fails_on_demand() {
475 use ratatui::backend::Backend;
476
477 let mut ok = TestBackendHarness::new(10, 3);
478 assert!(ok.draw(std::iter::empty()).is_ok());
479 assert!(ok.hide_cursor().is_ok());
481 assert!(ok.show_cursor().is_ok());
482 assert!(ok.get_cursor_position().is_ok());
483 assert!(
484 ok.set_cursor_position(ratatui::layout::Position::new(0, 0))
485 .is_ok()
486 );
487 assert!(ok.clear().is_ok());
488 assert!(ok.clear_region(ratatui::backend::ClearType::All).is_ok());
489 assert!(ok.size().is_ok());
490 assert!(ok.window_size().is_ok());
491 assert!(ok.flush().is_ok());
492
493 let mut bad = TestBackendHarness::failing(10, 3);
494 assert!(bad.draw(std::iter::empty()).is_err());
495 }
496
497 #[test]
498 fn test_terminal_is_ready_to_draw() {
499 let mut terminal = test_terminal();
500 assert!(terminal.draw(|_| {}).is_ok());
501 }
502
503 #[test]
504 fn test_setup_succeeds_by_default_and_fails_when_switched() {
505 let mut setup = TestSetup::new();
506 assert!(setup.enable().is_ok());
507 assert!(setup.create_terminal().is_ok());
508 setup.disable();
509 setup.print_done();
510
511 let mut enable_fails = TestSetup {
512 enable_should_fail: true,
513 create_should_fail: false,
514 draw_should_fail: false,
515 };
516 assert!(enable_fails.enable().is_err());
517
518 let mut create_fails = TestSetup {
519 enable_should_fail: false,
520 create_should_fail: true,
521 draw_should_fail: false,
522 };
523 assert!(create_fails.create_terminal().is_err());
524 }
525}