rusttuicrosstermratatui

How can I handle blocking input and a fixed UI tick rate?


I am building a TUI and need my main loop to handle two things: redraw the UI on a fixed timer (a "tick") and immediately handle user input.

My current loop uses crossterm::event::read(), but it's a blocking call, which means the UI only updates after the user presses a key.

This approach doesn't work for showing real-time updates like a clock.

What is the idiomatic Rust pattern for an event loop that handles both timed ticks and blocking input?

I've seen that crossterm::event::poll has a timeout, which seems promising. Is using poll the standard way to solve this, or is there a better, more robust pattern?

Here is a Minimal, Reproducible Example (MRE) of my problem:

Cargo.toml:

[dependencies]
crossterm = "0.27"

main.rs:


use crossterm::event::{self, Event, KeyCode};
use std::io::{self, Write};
use std::time::{Duration, Instant};

fn main() -> io::Result<()> {
    println!("Starting event loop. Press 'q' to quit.");
    println!("Notice how the 'tick' message only appears AFTER you press a key.");

    let mut last_tick = Instant::now();
    let tick_rate = Duration::from_secs(1);

    loop {
        // This is a blocking call. The loop will pause here until an event occurs.
        let event = event::read()?; 

        println!("- Event received: {:?}", event);

        if let Event::Key(key) = event {
            if key.code == KeyCode::Char('q') {
                println!("'q' pressed, quitting.");
                break;
            }
        }

        // This part of the code is only reached AFTER the blocking `event::read()` returns.
        if last_tick.elapsed() >= tick_rate {
            println!("Tick!");
            io::stdout().flush()?;
            last_tick = Instant::now();
        }
    }

    Ok(())
}

When you run this code, you will see that the "Tick!" message is only printed to the console after you press a key, which demonstrates the blocking issue I need to solve. I am looking for the standard Rust pattern to handle both the timed "Tick!" and the user input concurrently.


Solution

  • I am looking for the standard Rust pattern to handle both the timed "Tick!" and the user input concurrently.

    A fundamental characteristic of blocking function calls is that there is no general way to multiplex them with any other activity on a single thread — they tie up the thread doing the one thing, and so in order to handle some other event/process concurrently, you have to do one of these things: