Skip to content

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:

src/events.rs
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:

src/events.rs
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.

src/events.rs
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:

src/events.rs
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:

src/main.rs
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