Skip to content

Program

Program<I, O> runs an interactive terminal session. It holds the connection to the terminal, turns arriving bytes into events, remembers what the terminal said about itself and which modes it switched on, and owns a Screen for the drawing. Drawing still happens on Screen; Program is everything around it.

What it owns

    flowchart TB
  program["Program"] --> terminal["The terminal connection"]
  program --> events["Keys, mouse, and other events"]
  program --> modes["The modes it switched on"]
  program --> caps["What the terminal said about itself"]
  program --> screen["Screen: the renderer"]
  

Creating a Program does nothing to the terminal. It takes the input and output handles and sizes the renderer to fit, and that is all. Starting the session and asking the terminal any questions both wait for you to ask.

  • Program::stdio() uses the process’s own input and output.
  • Program::open() talks to the terminal directly, which is what you want when input or output has been redirected.
  • Program::new(terminal) builds on a terminal you already have.

A minimal session

use uncurses::event::Event;
use uncurses::program::Program;
use uncurses::style::Style;
use uncurses::text::TextSurface;

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

    let screen = program.screen_mut();
    screen.set_str((0, 0), "hello, uncurses!", Style::default());
    screen.render()?;

    while !matches!(program.read_event()?, Event::KeyPress(_)) {}
    program.finish()
}

init() starts the session. It puts the terminal into raw mode, so keystrokes arrive as you press them instead of a line at a time, and sizes the drawing area to fit. finish() ends the session, switching off everything the program switched on and handing the terminal back as it found it. Nothing tidies up on its own, so call finish() when you are done.

pause() hands the terminal back temporarily, which is what you want before running something like an editor, while keeping your program alive. resume() takes it back and forgets what was on screen, so your next render() draws everything. On Unix, suspend() pauses and then stops the process the way Ctrl+Z does; call resume() once it starts again.

Drawing through the screen

Drawing lives on Screen. Borrow it with screen() or screen_mut(), and keep the binding for as long as you are drawing:

loop {
    let screen = program.screen_mut();
    screen.set_str((0, 0), "ready", Style::default());
    screen.render()?;

    if matches!(program.read_event()?, Event::KeyPress(_)) {
        break;
    }
}

The borrow ends at the binding’s last use, so program is usable again on the next line, including inside an event loop like this one. Reach for program.screen_mut() inline when you only have a single call to make, such as a draw call in a match arm.

Drawing works here exactly as it does on any other surface. The screen is still just a grid of cells that knows how to paint itself; the program only owns it for the length of the session.

Program drives the screen

Some of what a program turns on changes how the screen should draw, and Program is what keeps the two in agreement. Moving to the alt screen is the clearest case: the terminal has to switch buffers, and the screen has to know it now covers the whole window instead of a few rows. Showing and hiding the cursor is the other.

Program does both halves in a single call. enter_alt_screen() moves the terminal and tells the screen; hide_cursor() hides the cursor and tells the screen. Reach for these rather than setting the screen’s half yourself, or the two end up disagreeing about what the terminal is actually doing.

Some of what Program learns is between it and the terminal alone: mouse reporting, bracketed paste, and the window title change nothing about how a frame is drawn, so the screen is never told. Some of it does reach the screen, though never as a mode the screen emits: an environment-derived color profile, tab and backspace optimizations after raw mode, and synchronized output once a mode report proves the terminal understands it. The Program API reference has the full set.

Reading events

Input belongs to Program. It can block for the next event, wait with a timeout, or take one without blocking, whichever suits the loop you are writing.

Reads through Program observe what they return, which is what keeps capabilities, the window size, and the renderer current without any bookkeeping on your part. If you take events from the shared event source or an async stream instead, hand each one to observe_event so the same updates still happen. See Events.

Observing records, it never queries. Nothing in the read path asks the terminal a question, so no reply reaches your loop that you did not ask for. The flip side is that anything a terminal only reports on request, such as the pixel sizes and the inline origin, goes stale until you ask again.

Startup options

ProgramOptions carries the startup behavior that init applies for you, so things like bracketed paste or mouse reporting are on before your first frame rather than being separate calls after it.

Three options act on what the terminal reports about itself instead of being emitted outright, which means they stay dormant until you query. That story lives with the rest of discovery in Capabilities, along with everything a terminal can tell you and how to ask.