pub mod event; pub mod handler; pub mod listener; pub mod ui; mod lib; use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}; use ratatui::backend::Backend; use ratatui::Terminal; use std::io; use std::marker::PhantomData; use self::event::EventError; use self::handler::IEventHandler; use self::listener::IEventListener; use self::ui::IUi; #[derive(Debug, PartialEq, Eq)] pub enum Error { Lib(String), Io(String), Event(String), ListenerPanic, } impl From for Error { fn from(err: musichoard::Error) -> Error { Error::Lib(err.to_string()) } } impl From for Error { fn from(err: io::Error) -> Error { Error::Io(err.to_string()) } } impl From for Error { fn from(err: EventError) -> Error { Error::Event(err.to_string()) } } pub struct Tui { terminal: Terminal, _phantom: PhantomData, } impl Tui { fn init(&mut self) -> Result<(), Error> { self.terminal.hide_cursor()?; self.terminal.clear()?; Ok(()) } fn exit(&mut self) -> Result<(), Error> { self.terminal.show_cursor()?; Ok(()) } #[allow(unused_must_use)] fn exit_suppress_errors(&mut self) { self.exit(); } fn main_loop(&mut self, mut ui: UI, handler: impl IEventHandler) -> Result<(), Error> { while ui.is_running() { self.terminal.draw(|frame| ui.render(frame))?; handler.handle_next_event(&mut ui)?; } Ok(()) } fn main( term: Terminal, ui: UI, handler: impl IEventHandler, listener: impl IEventListener, ) -> Result<(), Error> { let mut tui = Tui { terminal: term, _phantom: PhantomData, }; tui.init()?; let listener_handle = listener.spawn(); let result = tui.main_loop(ui, handler); match result { Ok(_) => { tui.exit()?; Ok(()) } Err(err) => { // We want to call exit before handling the result to reset the terminal. Therefore, // we suppress exit errors (if any) to not mask the original error. tui.exit_suppress_errors(); if listener_handle.is_finished() { match listener_handle.join() { Ok(err) => return Err(err.into()), // Calling std::panic::resume_unwind(err) as recommended by the Rust docs // will not produce an error message. The panic error message is printed at // the location of the panic which at the time is hidden by the TUI. Err(_) => return Err(Error::ListenerPanic), } } Err(err) } } } // GRCOV_EXCL_START fn enable() -> Result<(), Error> { terminal::enable_raw_mode()?; crossterm::execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture)?; Ok(()) } fn disable() -> Result<(), Error> { terminal::disable_raw_mode()?; crossterm::execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture)?; Ok(()) } #[allow(unused_must_use)] fn disable_suppress_errors() { Self::disable(); } pub fn run( term: Terminal, ui: UI, handler: impl IEventHandler, listener: impl IEventListener, ) -> Result<(), Error> { Self::enable()?; let result = Self::main(term, ui, handler, listener); match result { Ok(_) => { Self::disable()?; Ok(()) } Err(err) => { // We want to call disable before handling the result to reset the terminal. // Therefore, we suppress disable errors (if any) to not mask the original error. Self::disable_suppress_errors(); Err(err) } } } // GRCOV_EXCL_STOP } #[cfg(test)] mod tests { use std::{io, thread}; use musichoard::Collection; use ratatui::{backend::TestBackend, Terminal}; use crate::tests::COLLECTION; use super::{ event::EventError, handler::MockIEventHandler, lib::MockIMusicHoard, listener::MockIEventListener, ui::{IUi, Ui}, Error, Tui, }; pub fn terminal() -> Terminal { let backend = TestBackend::new(150, 30); Terminal::new(backend).unwrap() } pub fn music_hoard(collection: Collection) -> MockIMusicHoard { let mut music_hoard = MockIMusicHoard::new(); music_hoard.expect_load_from_database().returning(|| Ok(())); music_hoard.expect_rescan_library().returning(|| Ok(())); music_hoard.expect_get_collection().return_const(collection); music_hoard } pub fn ui(collection: Collection) -> Ui { Ui::new(music_hoard(collection)).unwrap() } fn listener() -> MockIEventListener { let mut listener = MockIEventListener::new(); listener.expect_spawn().return_once(|| { thread::spawn(|| { thread::park(); EventError::Io(io::Error::new(io::ErrorKind::Interrupted, "unparked")) }) }); listener } fn handler() -> MockIEventHandler> { let mut handler = MockIEventHandler::new(); handler .expect_handle_next_event() .return_once(|ui: &mut Ui| { ui.quit(); Ok(()) }); handler } #[test] fn run() { let terminal = terminal(); let ui = ui(COLLECTION.to_owned()); let listener = listener(); let handler = handler(); let result = Tui::main(terminal, ui, handler, listener); assert!(result.is_ok()); } #[test] fn event_error() { let terminal = terminal(); let ui = ui(COLLECTION.to_owned()); let listener = listener(); let mut handler = MockIEventHandler::new(); handler .expect_handle_next_event() .return_once(|_| Err(EventError::Recv)); let result = Tui::main(terminal, ui, handler, listener); assert!(result.is_err()); assert_eq!( result.unwrap_err(), Error::Event(EventError::Recv.to_string()) ); } #[test] fn listener_error() { let terminal = terminal(); let ui = ui(COLLECTION.to_owned()); let error = EventError::Io(io::Error::new(io::ErrorKind::Interrupted, "error")); let listener_handle: thread::JoinHandle = thread::spawn(|| error); while !listener_handle.is_finished() {} let mut listener = MockIEventListener::new(); listener.expect_spawn().return_once(|| listener_handle); let mut handler = MockIEventHandler::new(); handler .expect_handle_next_event() .return_once(|_| Err(EventError::Recv)); let result = Tui::main(terminal, ui, handler, listener); assert!(result.is_err()); let error = EventError::Io(io::Error::new(io::ErrorKind::Interrupted, "error")); assert_eq!(result.unwrap_err(), Error::Event(error.to_string())); } #[test] fn listener_panic() { let terminal = terminal(); let ui = ui(COLLECTION.to_owned()); let listener_handle: thread::JoinHandle = thread::spawn(|| panic!()); while !listener_handle.is_finished() {} let mut listener = MockIEventListener::new(); listener.expect_spawn().return_once(|| listener_handle); let mut handler = MockIEventHandler::new(); handler .expect_handle_next_event() .return_once(|_| Err(EventError::Recv)); let result = Tui::main(terminal, ui, handler, listener); assert!(result.is_err()); assert_eq!(result.unwrap_err(), Error::ListenerPanic); } #[test] fn errors() { let lib_err: Error = musichoard::Error::DatabaseError(String::from("")).into(); let io_err: Error = io::Error::new(io::ErrorKind::Interrupted, "error").into(); let event_err: Error = EventError::Recv.into(); let listener_err = Error::ListenerPanic; assert!(!format!("{:?}", lib_err).is_empty()); assert!(!format!("{:?}", io_err).is_empty()); assert!(!format!("{:?}", event_err).is_empty()); assert!(!format!("{:?}", listener_err).is_empty()); } }