How does one decide when crossterm::EventStream should be used over simple crossterm::Event with read()?

What are the use cases of EventStream, and when should I use it, just want to understand when to use this functionality over a simple read() of events.

Seems pretty self-explanatory: EventStream in crossterm::event - Rust

It implements the Stream trait and allows you to receive Events with async-std or tokio crates.

EvenStream is async while read is blocking.

Hi, thanks for the answer,

I have another question.
I am working on a multiplayer game as a side project. Some context:

I had implemented an EventStream to detect the keypresses made by the players, which now when I look back, seems like it could have been achieved by just simply using read().

Sharing the code here for reference

pub fn new() -> Self {
        let (ltx, lrx) = tokio::sync::mpsc::unbounded_channel::<KeyPress>();

        let t_rate = Duration::from_millis(250);
        let _tx: tokio::sync::mpsc::UnboundedSender<KeyPress> = ltx.clone(); 

        let _ = tokio::spawn(async move{
            let mut reader = crossterm::event::EventStream::new(); 
            let mut inter = tokio::time::interval(t_rate);

            loop {
                let delay = inter.tick();
                let crossterm_event = reader.next().fuse(); 

                // select uses both the async functions. 
                tokio::select! {
                    maybe_event = crossterm_event => {
                        match maybe_event {
                            Some(Ok(evt)) => {
                                match evt {
                                    crossterm::event::Event::Key(key) => {
                                        if key.kind == crossterm::event::KeyEventKind::Press {
                                            // process the key here. 
                                            println!("key pressed {:?}", &key);
                                            ltx.send(KeyPress::Key(key)).unwrap();
                                        }
                                    }
                                    _ => {
                                        // for all the other crossterm events. 
                                    }
                                }
                            },
                            Some(Err(_)) => {
                                ltx.send(KeyPress::Error).unwrap();

                            }
                            None => {
                                println!("None");
                            }
                        }
                    }, 
                    _ = delay => {
                        // panic here for some reason. 
                        // maybe because I was pushing this too quick . 
                        // understand the select macro a bit better. 
                        println!("ticking");
                        ltx.send(KeyPress::Tick).unwrap();
                    },
                }
            }
        });

        KeyboardEvent{_tx,rx: lrx }
    }

What do you think? This is my first time working on this kind of project in Rust, so any kind of feedback is welcome.

Thanks a lot.

From the perspective of usage for read() vs EventSteam:

  • read() is a blocking function, meaning once the program reaches read, it'll wait the user to trigger some event to let itself proceed.
    • to aviod the blocking behavior, poll() should be used to check wether an event is available in a given time period: poll() is not blocking, it just returns false if the event is empty after the time out; or returns true when the event is ready and you can call read() to read the event
    • poll + read is only usable on the same thread: if your program is multithreaded, make sure to obey the prerequisite
    • example: Hello world | Ratatui
  • EventStream is feature-gated behind event-stream: it's a stream of values produced asynchronously. So to use it, you have to set up a async runtime, and program in async Rust. It won't block the thread, and is suitable if your program affords to bring async code.

It’s not allowed to call these functions from different threads or combine them with the EventStream.
src: crossterm::event - Rust

Hello, first of all, thank you for your input.

  1. I have used async runtime, so I believe it was the right call to use EventStream.
  2. Nonetheless, when running the code and debugging it, I notice that I am not getting the events instantaneously.
pub enum Action {
    DIRECTION(MOVEMENT),
    QUIT,
    WRITE(char),
    NONE,
}

pub enum KeyPress {
    Key(crossterm::event::KeyEvent),
    Error, 
    Tick,
}


// Now this will be our stream source. 
pub struct KeyboardEvent {
    _tx: tokio::sync::mpsc::UnboundedSender<KeyPress>,
    rx: tokio::sync::mpsc::UnboundedReceiver<KeyPress>,
    // task: Option<tokio::task::JoinHandle<()>>,
}

Will these events like add up? And only be processed one after another?

Thanks

EventStream being async is what allows you to use it in the select! macro, which allows you to wait for it and your interval simultaneously. You can put as many event sources in select! as you want, like network and file I/O. You could also architect it so that you never need channels: events could be handled in the same loop as select!. And Tokio allows you to run your task in the background (tokio::spawn) instead of forwarding the execution to the function caller (.await). It also allows you to choose a single-threaded or multithreaded runtime without needing to change your code.

I'm not sure how much delay crossterm has, but if there's any problems on your end it would be when you are consuming the channel, like keyboard_event.rx.recv().await.

1 Like

Thanks a lot, I am trying to find the problem, I guess, either it will be this, or the Channels. Thanks for all the help.

So I have narrowed down the problem, in my case the problem happens to be from the server end, the server is not returning the response, and the client is doing fine generating the events.

1 Like

For something a bit more easier to read / understand, you might consider the approach that we used in crates-tui - crates-tui/src/events.rs at main · ratatui-org/crates-tui · GitHub

impl Events {
    pub fn new() -> Self {
        Self {
            streams: StreamMap::from_iter([
                (StreamName::Ticks, tick_stream()),
                (StreamName::KeyRefresh, key_refresh_stream()),
                (StreamName::Render, render_stream()),
                (StreamName::Crossterm, crossterm_stream()),
            ]),
        }
    }

    pub async fn next(&mut self) -> Option<Event> {
        self.streams.next().await.map(|(_name, event)| event)
    }
}

fn tick_stream() -> Pin<Box<dyn Stream<Item = Event>>> {
    let tick_delay = Duration::from_secs_f64(1.0 / config::get().tick_rate);
    let tick_interval = interval(tick_delay);
    Box::pin(IntervalStream::new(tick_interval).map(|_| Event::Tick))
}

fn key_refresh_stream() -> Pin<Box<dyn Stream<Item = Event>>> {
    let key_refresh_delay = Duration::from_secs_f64(1.0 / config::get().key_refresh_rate);
    let key_refresh_interval = interval(key_refresh_delay);
    Box::pin(IntervalStream::new(key_refresh_interval).map(|_| Event::KeyRefresh))
}

fn render_stream() -> Pin<Box<dyn Stream<Item = Event>>> {
    let render_delay = Duration::from_secs_f64(1.0 / config::get().frame_rate);
    let render_interval = interval(render_delay);
    Box::pin(IntervalStream::new(render_interval).map(|_| Event::Render))
}

fn crossterm_stream() -> Pin<Box<dyn Stream<Item = Event>>> {
    Box::pin(EventStream::new().fuse().filter_map(|event| async move {
        match event {
            // Ignore key release / repeat events
            Ok(CrosstermEvent::Key(key)) if key.kind == KeyEventKind::Release => None,
            Ok(event) => Some(Event::Crossterm(event)),
            Err(_) => Some(Event::Error),
        }
    }))
}
2 Likes

Thanks a lot, @joshka, your comment has highlighted some of the things I am still not using in Rust properly, such as Pin. I will study your suggestions and try incorporating them in the coming days. Thanks again.

1 Like

No problem. The other piece of advice I'd offer (especially if you're new to rust) is that if you aren't calling code that needs async, it can be much simpler in the context of a Ratatui app avoid async. If you do need async however, make sure to read the tokio tutorial.

Hey Joshka, I am not new to Rust, but I am in no way an expert at it. I have made some stuff using Ratatui(such as GitHub - supreetsingh10/noterm: Making sticky notes for the terminal in rust), but I am new to the concurrency aspects of the language. I'm just trying to get better at the art of programming.