Skip to content

The layers

Screen is the front door for most apps. It is assembled from smaller pieces, and each piece is usable on its own when your use case calls for it. This page maps those roles and shows when to reach for each one.

    flowchart TB
  Screen["Screen<br/>managed session + diffed drawing"]
  Screen --> Terminal["Terminal<br/>raw mode, window size, I/O handles"]
  Screen --> EventSource["EventSource<br/>typed input decoder"]
  TextBuffer["TextBuffer<br/>off-screen cell grid"] --> Encode["Encode<br/>surface to escape bytes"]
  

uncurses gives you two common routes. Screen manages a terminal session: it owns the terminal, decodes input, and diffs frames so only changed cells hit the wire. TextBuffer is for off-screen output: you paint whole frames and serialize them to bytes yourself, with no session involved. Terminal and EventSource are the pieces Screen uses for raw-mode terminal access and input decoding, and you can use them directly when that is the right fit.

Screen

The managed terminal session and home of diffed drawing. Screen owns a Terminal and an EventSource, tracks what is currently on the terminal across frames, and emits only the cells that changed. It also handles raw mode, capability detection, a sensible set of default modes, and teardown. Drive it inline or fullscreen.

let mut screen = Screen::stdio()?;
screen.init()?;
screen.set_str((0, 0), "managed and diffed", Style::new());
screen.render()?;
screen.finish()

Reach for Screen to build an interactive app: anything with an event loop, a changing display, and a terminal it should leave spotless on exit. If you are not sure which layer you want, it is this one.

TextBuffer

An off-screen frame buffer. A TextBuffer, or any surface grid, is a structured grid of cells you paint complete frames into and compose before sending them anywhere. There is no diffing and no terminal session; it owns neither input nor output, so it never touches raw mode. When a frame is ready, the Encode trait serializes it to bytes you write wherever you like: a terminal, a pipe, a file, or a string.

let mut frame = TextBuffer::new(80, 24);
frame.set_str((0, 0), "rendered once", Style::new());
let bytes = frame.display().to_string();

By default, encoding uses true color. To choose another color profile, the *_with variants take a color Profile: encode_with and display_with downsample to Ansi256 or Ansi, or strip styling entirely. Profile::Ascii keeps attributes but drops color, and Profile::Disabled produces plain text with no escape sequences, which is useful for logs, diffs, and snapshot tests.

// Plain text, no ANSI escapes.
let plain = frame.display_with(Profile::Disabled).to_string();

Composing frames this way fits one-shot output, transcripts, golden tests, and append-style printing, anywhere a live, diffed session would get in the way.

EventSource

The input decoder. An EventSource reads raw bytes from an input handle and decodes them into structured Event values: keypresses, mouse events, paste, focus changes, and resizes. That is its entire job. It does not draw, render, or touch the output side at all; it turns terminal input into types you can match on. It is exactly what Screen uses under the hood to read events.

let mut term = Terminal::stdio();
term.make_raw()?;
let mut events = EventSource::new(term.input())?;

match events.read()? {
    Event::KeyPress(key) => { /* handle it */ }
    _ => {}
}

term.restore()

Three ways to pull events: read() blocks until one arrives, poll(timeout) waits until one is queued or the timeout expires, and try_read() returns the next queued event without blocking. With the async feature, into_stream() turns the source into an EventStream. On Screen, these are spelled read_event, poll_event, and try_read_event, with event_stream() for async loops over the screen’s own decoder. Screen event reads are pure: feeding each one to screen.observe_event(&ev)? is optional and keeps capability detection, resize handling, and discovery defaults alive, and skipping it still reads fine. Reach for a bare EventSource when you need decoded terminal input on its own, separate from the drawing and session that Screen bundles around it.

Terminal

The device handle. Terminal owns the connection to the tty: it enters and leaves raw mode, queries the window size, snapshots the environment, and exposes copyable input and output handles you can hand to the other pieces. make_raw() stashes the prior state so restore() can put it back with no arguments.

let mut term = Terminal::stdio();
term.make_raw()?;
let size = term.get_window_size().unwrap_or_default();
// hand term.input() / term.output() to the other pieces
term.restore()

You rarely start here unless you are assembling your own version of Screen, or you need the raw device for something uncurses does not wrap. Most of the time, Screen holds the Terminal for you.

Which layer

You want to…Reach for
Build an interactive app, inline or fullscreenScreen
Produce a frame to print, log, snapshot test, or pipeTextBuffer
Read and decode terminal input on its ownEventSource
Touch raw mode and the device, nothing moreTerminal

When in doubt, start with Screen. Move to the smaller pieces only when a specific need points there.

Next steps

With the map in place, the next page puts Screen to work and builds a small interactive app from an empty file: your first app.