Color
Color is part of a cell’s style. You set the color you want at full fidelity, and uncurses adapts it to what the terminal in front of you can actually display. You never branch on terminal capability; you state your intent, and the library does the downsampling.
Three depths
A Color has three representations, and you mix them freely:
| Kind | Range | Example |
|---|---|---|
| named | the 16 named ANSI colors | Color::Green, Color::BrightBlue |
Color::Indexed | the 256-color xterm palette | Color::Indexed(208) |
Color::Rgb | 24-bit true color | Color::Rgb(255, 105, 180) |
All three convert to RGB with to_rgb, so palette colors work with the true-color
helpers. Color::hex parses rgb, rrggbb, or rrggbbaa, with or without a
leading # (alpha is ignored, terminals have none), and Color::hsl builds a
color from hue, saturation, and lightness.
Profiles and downsampling
A Profile is the color capability of
an output stream. There are five, ordered from least to most capable:
Disabled < Ascii < Ansi < Ansi256 < TrueColorWhen a frame is rendered, every color is mapped through the active profile to the best thing that profile can emit. You always specify the color you mean; the profile decides how it comes out.
flowchart TB
color["a color you set (Rgb / Indexed / named)"]
color --> tc["TrueColor: the original color"]
color --> a256["Ansi256: nearest palette index"]
color --> ansi["Ansi: nearest of the 16 named colors"]
color --> off["Ascii / Disabled: no color"]
Ascii drops color but keeps attributes like bold and underline; Disabled
drops styling entirely. That is the difference between a frame that is monochrome
but still has structure, and one that is plain text.
Detection follows your reads
A Screen picks an initial profile when you call init. It starts from the
environment, the same way other CLI tools decide whether to emit color. With
ScreenOptions::query_capabilities
left true, it also probes the terminal once for direct-color support. The
upgrade to TrueColor happens when your read loop feeds the terminal’s
XTGETTCAP reply through
screen.observe_event(&ev)?.
You can suppress that probe by setting the field to false. The environment
conventions it reads:
- Output that is not a TTY is
Disabled, unlessTTY_FORCEmakes it follow TTY rules orCLICOLOR_FORCEforces color. - On a TTY,
NO_COLORclamps toAscii: no color, but decoration may remain. COLORTERM=truecolor,24bit,yes, ortrueupgrades toTrueColor, except insidescreen.TERM=dumbisDisabled;*-256color,tmux*, andscreen*areAnsi256;*-directand known true-color terminal names areTrueColor.CLICOLORraises a non-dumb TTY to at leastAnsi;CLICOLOR_FORCEraises any output to at leastAnsi.
Read the result with screen.color_profile(), and override it with
screen.set_color_profile(..) when you want to force a level regardless of the
environment.
Screen reads are pure. Calling screen.observe_event(&ev)? on each event
is optional; it keeps capability tracking alive, and skipping it still reads
fine. The ratatui UncursesBackend follows the same pure-read contract.Choosing a profile yourself
When you are not using a Screen, the
Encode trait’s *_with variants take a
profile directly, so the same painted buffer can
produce full-color escapes for the terminal and plain text for a snapshot test:
use uncurses::buffer::TextBuffer;
use uncurses::color::{Color, Profile};
use uncurses::style::Style;
use uncurses::text::{Encode, TextSurface};
let mut buffer = TextBuffer::new(6, 1);
buffer.set_str((0, 0), "hello", Style::new().fg(Color::Green));
let colored = buffer.display().to_string(); // TrueColor by default
let plain = buffer.display_with(Profile::Disabled).to_string(); // no escapes
This is the basis of the offscreen rendering guide: compose once, emit at whatever color level the destination needs.