Handle results

This commit is contained in:
Wojciech Kozlowski 2023-04-12 15:41:57 +02:00
parent 47aa6d2e96
commit 9fd09e9db2
4 changed files with 139 additions and 52 deletions

View File

@ -57,13 +57,17 @@ fn main() {
// Initialize the terminal user interface. // Initialize the terminal user interface.
let backend = CrosstermBackend::new(io::stdout()); let backend = CrosstermBackend::new(io::stdout());
let terminal = Terminal::new(backend).expect("failed to initialise terminal"); let terminal = Terminal::new(backend).expect("failed to initialise terminal");
let channel = EventChannel::new(); let channel = EventChannel::new();
let listener = EventListener::new(channel.sender()); let listener = EventListener::new(channel.sender());
let handler = EventHandler::new(channel.receiver()); let handler = EventHandler::new(channel.receiver());
let ui = Ui::new(); let ui = Ui::new();
let app = App::new(collection_manager).expect("failed to initialise app"); 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. // Run the TUI application.
tui.run(); tui.run().expect("failed to run tui");
} }

View File

@ -1,8 +1,39 @@
use crossterm::event::{self, Event as CrosstermEvent, KeyEvent, MouseEvent}; use crossterm::event::{self, Event as CrosstermEvent, KeyEvent, MouseEvent};
use std::sync::mpsc; 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<mpsc::SendError<Event>> for EventError {
fn from(err: mpsc::SendError<Event>) -> EventError {
EventError::SendError(err.0)
}
}
impl From<mpsc::RecvError> for EventError {
fn from(_: mpsc::RecvError) -> EventError {
EventError::RecvError
}
}
#[derive(Clone, Copy, Debug)]
pub enum Event { pub enum Event {
Key(KeyEvent), Key(KeyEvent),
Mouse(MouseEvent), Mouse(MouseEvent),
@ -43,44 +74,46 @@ impl EventChannel {
} }
impl EventSender { impl EventSender {
pub fn send(&self, event: Event) { pub fn send(&self, event: Event) -> Result<(), EventError> {
self.sender Ok(self.sender.send(event)?)
.send(event)
.expect("failed to send terminal event");
} }
} }
impl EventReceiver { impl EventReceiver {
pub fn recv(&self) -> Event { pub fn recv(&self) -> Result<Event, EventError> {
self.receiver Ok(self.receiver.recv()?)
.recv()
.expect("failed to receive terminal event")
} }
} }
pub struct EventListener { pub struct EventListener {
events: Option<EventSender>, events: EventSender,
} }
impl EventListener { impl EventListener {
pub fn new(events: EventSender) -> EventListener { pub fn new(events: EventSender) -> EventListener {
EventListener { EventListener { events }
events: Some(events),
}
} }
pub fn spawn(&mut self) { pub fn spawn(self) -> thread::JoinHandle<EventError> {
let sender = self.events.take().unwrap(); thread::spawn(move || {
thread::spawn(move || loop { loop {
// Put this inside an if event::poll {...} if the display needs to be refreshed on a // Put this inside an if event::poll {...} if the display needs to be refreshed on a
// periodic basis. See // periodic basis. See
// https://github.com/tui-rs-revival/rust-tui-template/blob/master/src/event.rs. // https://github.com/tui-rs-revival/rust-tui-template/blob/master/src/event.rs.
match event::read().expect("unable to read event") { match event::read() {
CrosstermEvent::Key(e) => sender.send(Event::Key(e)), Ok(event) => {
CrosstermEvent::Mouse(e) => sender.send(Event::Mouse(e)), if let Err(err) = match event {
CrosstermEvent::Resize(w, h) => sender.send(Event::Resize(w, h)), 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!(), _ => unimplemented!(),
} } {
}); return err;
}
}
Err(err) => return EventError::IoError(err),
};
}
})
} }
} }

View File

@ -1,6 +1,6 @@
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use super::{app::App, event::{Event, EventReceiver}}; use super::{app::App, event::{Event, EventReceiver, EventError}};
pub struct EventHandler { pub struct EventHandler {
events: EventReceiver, events: EventReceiver,
@ -11,12 +11,13 @@ impl EventHandler {
EventHandler { events } EventHandler { events }
} }
pub fn handle_next_event(&mut self, app: &mut App) { pub fn handle_next_event(&mut self, app: &mut App) -> Result<(), EventError> {
match self.events.recv() { match self.events.recv()? {
Event::Key(key_event) => Self::handle_key_event(app, key_event), Event::Key(key_event) => Self::handle_key_event(app, key_event),
Event::Mouse(_) => {} Event::Mouse(_) => {}
Event::Resize(_, _) => {} Event::Resize(_, _) => {}
} };
Ok(())
} }
fn handle_key_event(app: &mut App, key_event: KeyEvent) { fn handle_key_event(app: &mut App, key_event: KeyEvent) {

View File

@ -11,13 +11,16 @@ pub mod handler;
pub mod ui; pub mod ui;
use self::app::App; use self::app::App;
use self::event::EventListener; use self::event::{EventError, EventListener};
use self::handler::EventHandler; use self::handler::EventHandler;
use self::ui::Ui; use self::ui::Ui;
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {
CollectionError(String), CollectionError(String),
IoError(String),
EventError(String),
ListenerPanic,
} }
impl From<collection::Error> for Error { impl From<collection::Error> for Error {
@ -26,9 +29,21 @@ impl From<collection::Error> for Error {
} }
} }
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Error::IoError(err.to_string())
}
}
impl From<EventError> for Error {
fn from(err: EventError) -> Error {
Error::EventError(err.to_string())
}
}
pub struct Tui<B: Backend> { pub struct Tui<B: Backend> {
terminal: Terminal<B>, terminal: Terminal<B>,
listener: EventListener, listener: Option<EventListener>,
handler: EventHandler, handler: EventHandler,
ui: Ui, ui: Ui,
app: App, app: App,
@ -44,37 +59,71 @@ impl<B: Backend> Tui<B> {
) -> Self { ) -> Self {
Self { Self {
terminal, terminal,
listener, listener: Some(listener),
handler, handler,
ui, ui,
app, app,
} }
} }
fn init(&mut self) { fn init(&mut self) -> Result<(), Error> {
terminal::enable_raw_mode().unwrap(); terminal::enable_raw_mode()?;
crossterm::execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture).unwrap(); crossterm::execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture)?;
self.terminal.hide_cursor().unwrap(); self.terminal.hide_cursor()?;
self.terminal.clear().unwrap(); self.terminal.clear()?;
Ok(())
} }
pub fn run(&mut self) { fn run_loop(&mut self) -> Result<(), Error> {
self.init();
self.listener.spawn();
while self.app.is_running() { while self.app.is_running() {
self.terminal self.terminal
.draw(|frame| self.ui.render(&mut self.app, frame)) .draw(|frame| self.ui.render(&mut self.app, frame))?;
.unwrap(); self.handler.handle_next_event(&mut self.app)?;
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(); self.exit();
} }
fn exit(&mut self) {
terminal::disable_raw_mode().unwrap();
crossterm::execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture).unwrap();
self.terminal.show_cursor().unwrap();
}
} }