Skip to content
Querying the terminal

Querying the terminal

Terminals can answer questions about themselves: the background color, the pixel size of a cell, the current cursor position, and what features they support. uncurses models this as a request you send and a reply that comes back as an ordinary event.

The request and reply model

You write a request, the terminal writes an answer, and that answer arrives in the same event stream as keystrokes. There is no separate “query” channel.

    flowchart TB
  req["program.request_background_color()"]
  req --> term["terminal"]
  term --> ev["Event::BackgroundColor(color) in your event loop"]
  
Program::init() and Program::init_with() set the session up and ask nothing about what the terminal can do. They do write: raw mode, tab stops, and the options you turned on. The capability queries below are always a call you make.

Capability probing

Call program.query_capabilities(&extra_bytes)? when you want uncurses to ask for its standard capability set. It writes the default queries, then your extra bytes, then Primary Device Attributes last, and flushes. The Primary DA reply is the sentinel: because its request was sent last, seeing Event::PrimaryDeviceAttributes means every earlier reply in that batch has already been delivered.

query_capabilities only writes. Consuming the replies is your job, so bound the wait with poll_event(Some(timeout)). A silent terminal never answers, including the sentinel.

Reading those replies is not purely passive. A few are adopted as they pass through, each gated by a ProgramOptions field that defaults to true: prefer_synchronized_output, prefer_grapheme_clusters and prefer_in_band_resize. Set any of them to false to record the capability without acting on it.

What adoption does differs by capability. Grapheme-cluster mode and in-band resize are terminal modes, so adopting one emits the mode, records it in the program’s emitted-mode set, and finish() undoes it. Synchronized output is a render property: adoption only tells the screen to start wrapping frames in the 2026 markers, so nothing is written at adoption time and there is nothing for finish() to undo.

use std::time::{Duration, Instant};

use uncurses::event::Event;
use uncurses::program::Program;

fn main() -> std::io::Result<()> {
    let mut program = Program::stdio()?;
    program.init()?;
    program.query_capabilities(&[])?;

    let deadline = Instant::now() + Duration::from_millis(300);
    while let Some(timeout) = deadline.checked_duration_since(Instant::now()) {
        if !program.poll_event(Some(timeout))? {
            break;
        }
        if matches!(program.try_read_event()?, Some(Event::PrimaryDeviceAttributes(_))) {
            break;
        }
    }

    let _caps = program.capabilities();
    program.finish()
}

Reads on Program auto-observe, so the loop above updates capabilities() as it reads replies. An ordinary event loop gets the same benefit for free: if it keeps calling read_event, try_read_event, or poll_event plus a read, capability replies are applied as they arrive.

That covers every reply describing the terminal, not only the ones query_capabilities asks for. A request_background_color answered mid-session is recorded just the same, so you can read it back from capabilities() later instead of matching the event and storing it yourself. Sizes are the exception: window and cell geometry lives on Program as window_cells(), window_pixels(), and cell_pixels(), since it changes with every resize.

Asking one question

Program also has request_* methods for common queries. Each one sends the request and flushes; the reply shows up later as an Event you match on in your loop.

use std::time::{Duration, Instant};

use uncurses::event::Event;
use uncurses::program::Program;

fn main() -> std::io::Result<()> {
    let mut program = Program::stdio()?;
    program.init()?;
    program.request_background_color()?;

    let deadline = Instant::now() + Duration::from_millis(300);
    'wait: while let Some(timeout) = deadline.checked_duration_since(Instant::now()) {
        if !program.poll_event(Some(timeout))? {
            break;
        }
        while let Some(ev) = program.try_read_event()? {
            if let Event::BackgroundColor(color) = ev {
                let _ = color;
                break 'wait;
            }
        }
    }

    program.finish()
}

These cover the everyday questions: the foreground, background, cursor, and palette colors; the cell and window pixel size; the cursor position; the color scheme (dark or light); terminal visibility; mode state; clipboard contents; and feature probes like kitty keyboard and modify-other-keys. For the complete set, scan the request_* methods on Program in the API reference; each one documents the exact Event variant used for its reply.

Extra queries in the capability batch

If you want your own query to share the Primary DA sentinel, pass its bytes as extra. uncurses writes them after the default capability queries and before the sentinel, so the same drain loop covers both the built-in replies and yours.

use std::time::{Duration, Instant};

use uncurses::ansi::color::REQUEST_BACKGROUND_COLOR;
use uncurses::event::Event;
use uncurses::program::Program;

fn main() -> std::io::Result<()> {
    let mut program = Program::stdio()?;
    program.init()?;
    program.query_capabilities(REQUEST_BACKGROUND_COLOR)?;

    let deadline = Instant::now() + Duration::from_millis(300);
    'wait: while let Some(timeout) = deadline.checked_duration_since(Instant::now()) {
        if !program.poll_event(Some(timeout))? {
            break;
        }
        while let Some(ev) = program.try_read_event()? {
            match ev {
                Event::PrimaryDeviceAttributes(_) => break 'wait,
                _ => {}
            }
        }
    }

    // Recorded on the way through, so there is nothing to match on.
    let _bg = program.capabilities().background_color();
    program.finish()
}

Asking for a current setting

The queries above ask what a terminal can do. DECRQSS asks what it is doing right now: the active text attributes, the current cursor style. That makes it a different kind of question, so it sits outside the default capability set and you send it as an extra query.

use uncurses::ansi::status::write_decrqss;
use uncurses::event::{Event, SettingReport};
use uncurses::program::Program;

fn main() -> std::io::Result<()> {
    let mut program = Program::stdio()?;
    program.init()?;

    let mut query = Vec::new();
    write_decrqss(&mut query, "m")?; // "m" is SGR, the active text attributes
    program.query_capabilities(&query)?;

    let mut sgr = None;
    loop {
        match program.read_event()? {
            // The reply to the only DECRQSS we sent, so it answers "m".
            Event::SettingReport(SettingReport::Raw(body)) => sgr = Some(body),
            Event::SettingReport(SettingReport::Refused) => break,
            Event::PrimaryDeviceAttributes(_) => break,
            _ => {}
        }
    }

    let _ = sgr;
    program.finish()
}

This is the one reply the program does not record for you. Every other answer says what it is about, so it can be filed on its own: a mode report carries its mode, an XTGETTCAP reply its capability names. A DECRPSS reply carries neither. A refusal is a bare Refused that names nothing at all, and a success is a Raw holding the whole CSI sequence with the control function and its parameters run together, so ">4;2m" cannot be told apart from an SGR reply by inspection alone.

What the reply is about lives in the request, and the request is yours. Send one DECRQSS at a time, or track the order you sent them in, and match the replies up as they arrive.

Asking without a Program

Without Program, a request is just bytes you write to the terminal, and the reply comes back through an EventSource. The ansi module has named constants for common requests, but any escape you write works the same way. Send Primary DA last if you want a terminator, and use a deadline for terminals that do not answer.

The query example (cargo run --example query) shows this raw-byte pattern. The gradient example shows the usual app pattern: call program.query_capabilities(&[])?, keep reading events in the normal loop, and let Program’s auto-observation update capabilities as replies arrive.