mod app; mod event; mod handler; mod lib; mod listener; mod ui; pub use app::app::App; pub use event::EventChannel; pub use handler::EventHandler; pub use listener::EventListener; pub use ui::Ui; 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 crate::tui::{ app::app::{IAppAccess, IAppInteract}, event::EventError, handler::IEventHandler, listener::IEventListener, ui::IUi, }; #[derive(Debug, PartialEq, Eq)] pub enum Error { Io(String), Event(String), ListenerPanic, } 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<(UI, APP)>, } 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 app: APP, _ui: UI, handler: impl IEventHandler, ) -> Result<(), Error> { while app.is_running() { self.terminal.draw(|frame| UI::render(&mut app, frame))?; handler.handle_next_event(&mut app)?; } Ok(()) } fn main( term: Terminal, app: APP, 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(app, 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, app: APP, ui: UI, handler: impl IEventHandler, listener: impl IEventListener, ) -> Result<(), Error> { Self::enable()?; let result = Self::main(term, app, 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 testmod; #[cfg(test)] mod tests { use std::{io, thread}; use ratatui::{backend::TestBackend, Terminal}; use musichoard::collection::Collection; use crate::tui::{ app::app::App, handler::MockIEventHandler, lib::MockIMusicHoard, listener::MockIEventListener, ui::Ui, }; use super::*; use testmod::COLLECTION; pub fn terminal() -> Terminal { let backend = TestBackend::new(150, 30); Terminal::new(backend).unwrap() } 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 } fn app(collection: Collection) -> App { App::new(music_hoard(collection)) } 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(|app: &mut App| { app.force_quit(); Ok(()) }); handler } #[test] fn run() { let terminal = terminal(); let app = app(COLLECTION.to_owned()); let ui = Ui; let listener = listener(); let handler = handler(); let result = Tui::main(terminal, app, ui, handler, listener); assert!(result.is_ok()); } #[test] fn event_error() { let terminal = terminal(); let app = app(COLLECTION.to_owned()); let ui = Ui; let listener = listener(); let mut handler = MockIEventHandler::new(); handler .expect_handle_next_event() .return_once(|_| Err(EventError::Recv)); let result = Tui::main(terminal, app, 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 app = app(COLLECTION.to_owned()); let ui = Ui; 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, app, 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 app = app(COLLECTION.to_owned()); let ui = Ui; 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, app, ui, handler, listener); assert!(result.is_err()); assert_eq!(result.unwrap_err(), Error::ListenerPanic); } #[test] fn errors() { 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!("{:?}", io_err).is_empty()); assert!(!format!("{:?}", event_err).is_empty()); assert!(!format!("{:?}", listener_err).is_empty()); } }