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.
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");
}

View File

@ -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<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 {
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<Event, EventError> {
Ok(self.receiver.recv()?)
}
}
pub struct EventListener {
events: Option<EventSender>,
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 {
pub fn spawn(self) -> thread::JoinHandle<EventError> {
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)),
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),
};
}
})
}
}

View File

@ -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) {

View File

@ -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<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> {
terminal: Terminal<B>,
listener: EventListener,
listener: Option<EventListener>,
handler: EventHandler,
ui: Ui,
app: App,
@ -44,37 +59,71 @@ impl<B: Backend> Tui<B> {
) -> 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)?;
}
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();
}
fn exit(&mut self) {
terminal::disable_raw_mode().unwrap();
crossterm::execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture).unwrap();
self.terminal.show_cursor().unwrap();
}
}