use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}; use musichoard::collection; use ratatui::backend::Backend; use ratatui::Terminal; use std::io; pub mod app; pub mod event; pub mod handler; pub mod ui; use self::app::App; use self::event::{EventError, EventListener}; use self::handler::EventHandler; use self::ui::Ui; #[derive(Debug)] pub enum Error { CollectionError(String), IoError(String), EventError(String), ListenerPanic, } impl From for Error { fn from(err: collection::Error) -> Error { Error::CollectionError(err.to_string()) } } impl From for Error { fn from(err: io::Error) -> Error { Error::IoError(err.to_string()) } } impl From for Error { fn from(err: EventError) -> Error { Error::EventError(err.to_string()) } } pub struct Tui { terminal: Terminal, listener: Option, handler: EventHandler, ui: Ui, app: App, } impl Tui { pub fn new( terminal: Terminal, listener: EventListener, handler: EventHandler, ui: Ui, app: App, ) -> Self { Self { terminal, listener: Some(listener), handler, ui, app, } } fn init(&mut self) -> Result<(), Error> { terminal::enable_raw_mode()?; crossterm::execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture)?; self.terminal.hide_cursor()?; self.terminal.clear()?; Ok(()) } fn run_loop(&mut self) -> Result<(), Error> { while self.app.is_running() { self.terminal .draw(|frame| self.ui.render(&mut self.app, frame))?; self.handler.handle_next_event(&mut self.app)?; } Ok(()) } pub fn run(mut self) -> Result<(), Error> { self.init()?; let listener_handle = self.listener.take().unwrap().spawn(); let result = self.run_loop(); match result { Ok(_) => { self.exit()?; Ok(()) } Err(err) => { // We want to call exit before handling the run_loop result to reset the terminal. // Therefore, we suppress exit errors (if any) to not mask the original error. self.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) } } } fn exit(&mut self) -> Result<(), Error> { terminal::disable_raw_mode()?; crossterm::execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture)?; self.terminal.show_cursor()?; Ok(()) } #[allow(unused_must_use)] fn exit_suppress_errors(&mut self) { self.exit(); } }