Events
We’ve already discussed Events
and an EventHandler
extensively in the
counter app. And you can use the exact same approach in your
async
application. If you do so, you can ignore this section.
However, when using tokio
, you have a few more options available to choose from. In this tutorial,
you’ll see how to take advantage of tokio_stream
to create custom streams and fuse them together
to get async events.
Event enum
First, create a Event
enum, like before:
use crossterm::event::Event as CrosstermEvent;
#[derive(Clone, Debug)]pub enum Event { Error, Render, Crossterm(CrosstermEvent),}
This will represent all possible events you can receive from the Events
stream.
Crossterm stream
Next create a crossterm_stream
function:
use futures::StreamExt;
type Stream = std::pin::Pin<Box<dyn futures::Stream<Item = Event>>>;
fn crossterm_stream() -> Stream { use crossterm::event::EventStream; use crossterm::event::KeyEventKind; use CrosstermEvent::Key; Box::pin(EventStream::new().fuse().filter_map(|event| async move { match event { // Ignore key release / repeat events Ok(Key(key)) if key.kind == KeyEventKind::Release => None, Ok(event) => Some(Event::Crossterm(event)), Err(_) => Some(Event::Error), } }))}
Render stream
You can create stream using an IntervalStream
for generating Event::Render
events.
fn render_stream() -> Stream { const FRAME_RATE: f64 = 15.0; let render_delay = std::time::Duration::from_secs_f64(1.0 / FRAME_RATE); let render_interval = tokio::time::interval(render_delay); Box::pin( tokio_stream::wrappers::IntervalStream::new(render_interval) .map(|_| Event::Render), )}
Event stream
Putting it all together, make a Events
struct like so:
pub struct Events { streams: tokio_stream::StreamMap<&'static str, Stream>,}
impl Default for Events { fn default() -> Self { Self { streams: tokio_stream::StreamMap::from_iter([ ("render", render_stream()), ("crossterm", crossterm_stream()), ]), } }}
impl Events { pub fn new() -> Self { Self::default() }
pub async fn next(&mut self) -> Option<Event> { self.streams.next().await.map(|(_name, event)| event) }}
With that, you can create an instance of Events
using Events::new()
, and get the next event on
the stream using Events::next().await
.
Here’s the full ./src/events.rs
for your reference:
src/events.rs (click to expand)
use crossterm::event::Event as CrosstermEvent;
#[derive(Clone, Debug)]pub enum Event { Error, Render, Crossterm(CrosstermEvent),}
use futures::StreamExt;
type Stream = std::pin::Pin<Box<dyn futures::Stream<Item = Event>>>;
pub struct Events { streams: tokio_stream::StreamMap<&'static str, Stream>,}
impl Default for Events { fn default() -> Self { Self { streams: tokio_stream::StreamMap::from_iter([ ("render", render_stream()), ("crossterm", crossterm_stream()), ]), } }}
impl Events { pub fn new() -> Self { Self::default() }
pub async fn next(&mut self) -> Option<Event> { self.streams.next().await.map(|(_name, event)| event) }}
fn render_stream() -> Stream { const FRAME_RATE: f64 = 15.0; let render_delay = std::time::Duration::from_secs_f64(1.0 / FRAME_RATE); let render_interval = tokio::time::interval(render_delay); Box::pin( tokio_stream::wrappers::IntervalStream::new(render_interval) .map(|_| Event::Render), )}
fn crossterm_stream() -> Stream { use crossterm::event::EventStream; use crossterm::event::KeyEventKind; use CrosstermEvent::Key; Box::pin(EventStream::new().fuse().filter_map(|event| async move { match event { // Ignore key release / repeat events Ok(Key(key)) if key.kind == KeyEventKind::Release => None, Ok(event) => Some(Event::Crossterm(event)), Err(_) => Some(Event::Error), } }))}
Demo
Let’s make a very simple event loop TUI using this events
module. Update main.rs
to the
following:
mod crates_io_api_helper;mod errors;mod events;mod tui;
#[tokio::main]async fn main() -> color_eyre::Result<()> { errors::install_hooks()?;
let mut tui = tui::init()?;
let mut events = events::Events::new();
use crossterm::event::Event as CrosstermEvent; use crossterm::event::KeyCode::Esc;
while let Some(evt) = events.next().await { match evt { events::Event::Render => { tui.draw(|frame| { frame.render_widget( ratatui::widgets::Paragraph::new(format!( "frame counter: {}", frame.count() )), frame.size(), ); })?; } events::Event::Crossterm(CrosstermEvent::Key(key)) if key.code == Esc => { break } _ => (), } }
tui::restore()?;
Ok(())}
Run the code to see the frame counter increment based on the frame rate.
Experiment with different frame rates by modifying the interval stream for the render tick.
Your file structure should now look like this:
.├── Cargo.lock├── Cargo.toml└── src ├── crates_io_api_helper.rs ├── errors.rs ├── events.rs ├── main.rs └── tui.rs