diff --git a/src/main.rs b/src/main.rs index 8e242dc..149ca59 100644 --- a/src/main.rs +++ b/src/main.rs @@ -57,13 +57,17 @@ fn main() { // Initialize the terminal user interface. let backend = CrosstermBackend::new(io::stdout()); let terminal = Terminal::new(backend).expect("failed to initialise terminal"); + let channel = EventChannel::new(); let listener = EventListener::new(channel.sender()); let handler = EventHandler::new(channel.receiver()); + let ui = Ui::new(); + let app = App::new(collection_manager).expect("failed to initialise app"); - let mut tui = Tui::new(terminal, listener, handler, ui, app); + + let tui = Tui::new(terminal, listener, handler, ui, app); // Run the TUI application. - tui.run(); + tui.run().expect("failed to run tui"); } diff --git a/src/tui/event.rs b/src/tui/event.rs index 07cf770..dae98ea 100644 --- a/src/tui/event.rs +++ b/src/tui/event.rs @@ -1,8 +1,39 @@ use crossterm::event::{self, Event as CrosstermEvent, KeyEvent, MouseEvent}; use std::sync::mpsc; -use std::thread; +use std::{fmt, thread}; -#[derive(Clone, Copy)] +#[derive(Debug)] +pub enum EventError { + SendError(Event), + RecvError, + IoError(std::io::Error), +} + +impl fmt::Display for EventError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Self::SendError(ref e) => write!(f, "failed to send event: {e:?}"), + Self::RecvError => write!(f, "receive event call failed"), + Self::IoError(ref e) => { + write!(f, "an I/O error was triggered during event handling: {e}") + } + } + } +} + +impl From> for EventError { + fn from(err: mpsc::SendError) -> EventError { + EventError::SendError(err.0) + } +} + +impl From for EventError { + fn from(_: mpsc::RecvError) -> EventError { + EventError::RecvError + } +} + +#[derive(Clone, Copy, Debug)] pub enum Event { Key(KeyEvent), Mouse(MouseEvent), @@ -43,44 +74,46 @@ impl EventChannel { } impl EventSender { - pub fn send(&self, event: Event) { - self.sender - .send(event) - .expect("failed to send terminal event"); + pub fn send(&self, event: Event) -> Result<(), EventError> { + Ok(self.sender.send(event)?) } } impl EventReceiver { - pub fn recv(&self) -> Event { - self.receiver - .recv() - .expect("failed to receive terminal event") + pub fn recv(&self) -> Result { + Ok(self.receiver.recv()?) } } pub struct EventListener { - events: Option, + events: EventSender, } impl EventListener { pub fn new(events: EventSender) -> EventListener { - EventListener { - events: Some(events), - } + EventListener { events } } - pub fn spawn(&mut self) { - let sender = self.events.take().unwrap(); - thread::spawn(move || loop { - // Put this inside an if event::poll {...} if the display needs to be refreshed on a - // periodic basis. See - // https://github.com/tui-rs-revival/rust-tui-template/blob/master/src/event.rs. - match event::read().expect("unable to read event") { - CrosstermEvent::Key(e) => sender.send(Event::Key(e)), - CrosstermEvent::Mouse(e) => sender.send(Event::Mouse(e)), - CrosstermEvent::Resize(w, h) => sender.send(Event::Resize(w, h)), - _ => unimplemented!(), + pub fn spawn(self) -> thread::JoinHandle { + thread::spawn(move || { + loop { + // Put this inside an if event::poll {...} if the display needs to be refreshed on a + // periodic basis. See + // https://github.com/tui-rs-revival/rust-tui-template/blob/master/src/event.rs. + match event::read() { + Ok(event) => { + if let Err(err) = match event { + CrosstermEvent::Key(e) => self.events.send(Event::Key(e)), + CrosstermEvent::Mouse(e) => self.events.send(Event::Mouse(e)), + CrosstermEvent::Resize(w, h) => self.events.send(Event::Resize(w, h)), + _ => unimplemented!(), + } { + return err; + } + } + Err(err) => return EventError::IoError(err), + }; } - }); + }) } } diff --git a/src/tui/handler.rs b/src/tui/handler.rs index 20ef5cb..5d0effc 100644 --- a/src/tui/handler.rs +++ b/src/tui/handler.rs @@ -1,6 +1,6 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use super::{app::App, event::{Event, EventReceiver}}; +use super::{app::App, event::{Event, EventReceiver, EventError}}; pub struct EventHandler { events: EventReceiver, @@ -11,12 +11,13 @@ impl EventHandler { EventHandler { events } } - pub fn handle_next_event(&mut self, app: &mut App) { - match self.events.recv() { + pub fn handle_next_event(&mut self, app: &mut App) -> Result<(), EventError> { + match self.events.recv()? { Event::Key(key_event) => Self::handle_key_event(app, key_event), Event::Mouse(_) => {} Event::Resize(_, _) => {} - } + }; + Ok(()) } fn handle_key_event(app: &mut App, key_event: KeyEvent) { diff --git a/src/tui/mod.rs b/src/tui/mod.rs index 4f25ed4..9056c46 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -11,13 +11,16 @@ pub mod handler; pub mod ui; use self::app::App; -use self::event::EventListener; +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 { @@ -26,9 +29,21 @@ impl From for Error { } } +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: EventListener, + listener: Option, handler: EventHandler, ui: Ui, app: App, @@ -44,37 +59,71 @@ impl Tui { ) -> Self { Self { terminal, - listener, + listener: Some(listener), handler, ui, app, } } - fn init(&mut self) { - terminal::enable_raw_mode().unwrap(); - crossterm::execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture).unwrap(); - self.terminal.hide_cursor().unwrap(); - self.terminal.clear().unwrap(); + fn init(&mut self) -> Result<(), Error> { + terminal::enable_raw_mode()?; + crossterm::execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture)?; + self.terminal.hide_cursor()?; + self.terminal.clear()?; + Ok(()) } - pub fn run(&mut self) { - self.init(); - - self.listener.spawn(); + fn run_loop(&mut self) -> Result<(), Error> { while self.app.is_running() { self.terminal - .draw(|frame| self.ui.render(&mut self.app, frame)) - .unwrap(); - self.handler.handle_next_event(&mut self.app); + .draw(|frame| self.ui.render(&mut self.app, frame))?; + self.handler.handle_next_event(&mut self.app)?; } - self.exit(); + Ok(()) } - fn exit(&mut self) { - terminal::disable_raw_mode().unwrap(); - crossterm::execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture).unwrap(); - self.terminal.show_cursor().unwrap(); + 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(); } }