Async event loops
Most apps can read input synchronously with Screen::read_event,
Screen::try_read_event,
or Screen::poll_event. Those reads are pure now:
they take &self, return events, and do not update terminal capabilities.
Screen is pure
input. Feeding events back through screen.observe_event(&ev)? is optional; it
keeps discovery-driven defaults alive, and skipping it still reads fine.With the async feature, Screen::event_stream
gives you a futures_core::Stream of io::Result<Event> over the screen’s own
decoder. The stream is owned, not borrowed, so the same async task can read
input, observe events, resize, and render in one tokio::select!. The feature
is runtime-agnostic: it depends on futures-core, not tokio.
If you need the shared input source directly, Screen::event_source
returns the Arc<Mutex<EventSource<I>>>.
Enable the feature in Cargo.toml:
[dependencies]
uncurses = { git = "https://github.com/aymanbagabas/uncurses", features = ["async"] }The Screen::event_stream() path
This is the recommended path for async apps. It mirrors examples/examples/async_screen.rs:
terminal input and a frame timer share one select!, and the same task renders.
use std::time::Duration;
use tokio_stream::StreamExt;
use uncurses::buffer::SurfaceMut;
use uncurses::event::{Event, Key};
use uncurses::screen::{Screen, ScreenOptions};
use uncurses::style::Style;
use uncurses::terminal::{Stdin, Stdout};
use uncurses::text::TextSurface;
const FRAME: Duration = Duration::from_millis(16);
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut screen = Screen::stdio()?;
screen.init_with(ScreenOptions::default())?;
screen.enter_alt_screen()?;
screen.hide_cursor()?;
let result = run(&mut screen).await;
let finish = screen.finish();
result.and(finish)
}
async fn run(screen: &mut Screen<Stdin, Stdout>) -> std::io::Result<()> {
let quit_keys: [Key; 3] = ["q", "esc", "ctrl+c"].map(|s| s.parse().unwrap());
let mut events = screen.event_stream();
let mut ticker = tokio::time::interval(FRAME);
let mut frames = 0u64;
loop {
tokio::select! {
maybe = events.next() => {
let Some(ev) = maybe else { break };
let ev = ev?;
screen.observe_event(&ev)?;
match ev {
Event::KeyPress(ref key) if quit_keys.contains(key) => break,
Event::Resize(ws) => screen.resize((ws.col, ws.row)),
_ => {}
}
}
_ = ticker.tick() => {
frames += 1;
screen.clear();
screen.set_str((0, 0), &format!("async uncurses frame {frames}"), Style::default());
screen.render()?;
}
}
}
Ok(())
}The full game version is examples/examples/async_arcade.rs. It uses the same
shape, then adds async game tasks feeding messages into the render loop.
event_stream() and the sync readers share one source. Use one steady-state
reader. If you run a sync reader and the stream at the same time, each event
goes to whichever consumer drains it first, not both.Observing events is opt-in
The reads above are pure input. observe_event is the opt-in call that turns
events into capability state. Skip it and your app still reads input, including
resize events from the OS; you just forgo capability tracking, cached window
sizes, and discovery-driven defaults such as mouse and keyboard upgrades.
Cleaning up
You do not need to drop Screen::event_stream() before
Screen::finish,
Screen::pause, or
Screen::resume. The
stream shares the input source by handle and can stay live across those calls.
ratatui backend async input
With uncurses-ratatui’s async feature, the backend has the same pure async
read path. Use UncursesBackend::event_stream
and pair events you want tracked with
UncursesBackend::observe_event.
use tokio_stream::StreamExt;
use uncurses::terminal::{Stdin, Stdout};
use uncurses_ratatui::UncursesBackend;
async fn run(backend: &mut UncursesBackend<Stdin, Stdout>) -> std::io::Result<()> {
let mut events = backend.event_stream();
while let Some(ev) = events.next().await {
let ev = ev?;
backend.observe_event(&ev)?;
// Handle the event, then draw your ratatui frame.
}
Ok(())
}Low-level EventSource::into_stream()
Use this only when you are not using Screen. It is the by-hand path: put the
terminal in raw mode yourself, build an EventSource
over the input half, and turn it into an EventStream.
use futures_lite::StreamExt; // or tokio_stream::StreamExt
use uncurses::event::{Event, EventSource, Key};
use uncurses::terminal::Terminal;
async fn run() -> std::io::Result<()> {
let mut term = Terminal::stdio();
term.make_raw()?;
let quit: Key = "ctrl+c".parse().unwrap();
let mut events = EventSource::new(term.input())?.into_stream();
while let Some(event) = events.next().await {
match event? {
Event::KeyPress(ref k) if *k == quit => break,
_ => {}
}
// React and repaint through your own output half.
}
term.restore()
}