Skip to content
The ratatui backend

The ratatui backend

If you already write your UI with ratatui widgets, you do not have to give that up to get uncurses underneath. The uncurses-ratatui crate is a ratatui Backend that renders through an uncurses Screen: you keep writing widgets, and uncurses diffs the frames and writes only the necessary bytes.

Install

Add the backend alongside ratatui:

[dependencies]
uncurses-ratatui = { git = "https://github.com/aymanbagabas/uncurses" }
ratatui = "0.30"

The features mirror the core crate: unicode-rs (default), icu, and async.

Setup and teardown

try_init enters raw mode, switches to the alternate screen, and hands you a ready ratatui Terminal; restore restores the terminal state. One call each, bracketing your app.

use std::io;

fn main() -> io::Result<()> {
    let mut terminal = uncurses_ratatui::try_init()?;
    let result = run(&mut terminal);
    uncurses_ratatui::restore(&mut terminal);
    result
}

try_init returns an io::Result; if you would rather panic on failure, init gives you the terminal directly. There are *_with_options variants when you want to pass ratatui TerminalOptions and uncurses ScreenOptions, for example to skip the startup capability probe with query_capabilities: false.

use std::io;

use ratatui::{TerminalOptions, Viewport};
use uncurses_ratatui::ScreenOptions;

fn main() -> io::Result<()> {
    let screen_options = ScreenOptions {
        query_capabilities: false,
        ..ScreenOptions::default()
    };

    let mut terminal = uncurses_ratatui::try_init_with_options(
        TerminalOptions {
            viewport: Viewport::Fullscreen,
        },
        screen_options,
    )?;

    uncurses_ratatui::restore(&mut terminal);
    Ok(())
}

Drawing and input

terminal.draw is plain ratatui. Render any widget into the frame; uncurses turns the resulting buffer into the smallest possible update. Input comes from uncurses through the backend.

use uncurses::event::Event;
use uncurses_ratatui::DefaultTerminal;

fn run(terminal: &mut DefaultTerminal) -> std::io::Result<()> {
    loop {
        terminal.draw(|frame| {
            frame.render_widget("Hello from ratatui, via uncurses", frame.area());
        })?;

        let backend = terminal.backend_mut();
        if !backend.poll_event(None)? {
            continue;
        }
        let Some(ev) = backend.try_read_event() else {
            continue;
        };
        backend.observe_event(&ev)?;
        if let Event::KeyPress(_) = ev {
            break;
        }
    }
    Ok(())
}

backend_mut exposes the same read_event, poll_event, and try_read_event you use on a bare Screen, producing uncurses Event values. So your render layer is ratatui and your input layer is uncurses, each doing what it is best at. Features covered in the other guides, including mouse input, paste, async events, and terminal queries, work through the backend.

Backend reads are pure, exactly like a raw Screen’s. Pass each event to backend.observe_event(&ev)? to keep capability tracking alive (mouse, kitty keyboard, in-band resize, truecolor, grapheme); skip it and reads still work. The ratatui_* examples show the pattern.

Async input

With the async feature, the backend exposes its own event_stream() for a tokio::select! loop. Like the sync reads it yields events without observing them, so pair it with observe_event to keep capability tracking alive:

use std::time::Duration;

use tokio_stream::StreamExt;

let mut events = terminal.backend_mut().event_stream();
let mut tick = tokio::time::interval(Duration::from_millis(16));

loop {
    tokio::select! {
        maybe = events.next() => {
            let Some(ev) = maybe else { break };
            let ev = ev?;
            terminal.backend_mut().observe_event(&ev)?;
            // handle input
        }
        _ = tick.tick() => {
            terminal.draw(|frame| {
                frame.render_widget("async ratatui", frame.area());
            })?;
        }
    }
}

The stream shares the screen’s decoder by handle, so you can hold it and still call mutable backend methods in the same loop. See the async events guide for the full pattern.

See the ratatui_minimal example for the smallest complete program, and browse the other ratatui_* examples for more, such as ratatui_popup, ratatui_inline, and ratatui_user_input.