How to colour terminal output
Short answer: Print ANSI escape sequences around the text (\x1b[31m starts red, \x1b[0m resets) and leave them out entirely when the NO_COLOR environment variable is set.
From a fresh project — cargo new colour — replace src/main.rs with each block below and run it with cargo run -q. A terminal renders the escapes as colour, so to see the bytes themselves pipe the output through cat -v, which prints ESC as ^[.
The escape sequence
Section titled “The escape sequence”fn main() { println!("\x1b[31mred\x1b[0m");}$ cargo run -q | cat -v^[[31mred^[[0m\x1b is the escape character, byte 27, and [31m is a control sequence: [ opens it, 31 is the parameter, and m is the command that sets colour and style. 31 means foreground red. The trailing \x1b[0m resets everything to normal, and forgetting it is the classic bug:
fn main() { println!("\x1b[31mred"); println!("still red");}$ cargo run -q | cat -v^[[31mredstill redThe colour is a state change, not a wrapper. Nothing in the second line resets it, so a terminal keeps painting red until it meets a reset.
A helper
Section titled “A helper”fn paint(code: &str, text: &str) -> String { format!("\x1b[{code}m{text}\x1b[0m")}
fn main() { println!("{}", paint("1", "bold")); println!("{}", paint("31", "red")); println!("{}", paint("1;32", "bold green")); println!("{}", paint("4;34", "underline blue"));}$ cargo run -q | cat -v^[[1mbold^[[0m^[[31mred^[[0m^[[1;32mbold green^[[0m^[[4;34munderline blue^[[0mSeveral parameters go in one sequence, separated by ;, so 1;32 is bold and green at once. The codes worth remembering: 1 bold, 4 underline, 30–37 for the eight foreground colours (31 red, 32 green, 33 yellow, 34 blue), 40–47 for the same eight as backgrounds, and 90–97 for the bright variants. paint returns a String instead of printing, so it drops into any format!.
The NO_COLOR variable
Section titled “The NO_COLOR variable”Some readers pipe output into a pager, a log file, or a screen reader, where escape codes are noise. The informal standard at no-color.org asks software to check for one variable: when NO_COLOR is present and not an empty string, don’t add colour.
use std::env;
fn paint(colour: bool, code: &str, text: &str) -> String { if colour { format!("\x1b[{code}m{text}\x1b[0m") } else { text.to_string() }}
fn main() { let colour = !env::var("NO_COLOR").is_ok_and(|value| !value.is_empty()); println!("{} done", paint(colour, "32", "ok"));}$ cargo run -q | cat -v^[[32mok^[[0m done
$ NO_COLOR=1 cargo run -q | cat -vok done
$ NO_COLOR= cargo run -q | cat -v^[[32mok^[[0m doneenv::var returns Ok for a variable that is set at all, empty or not, so the check has to look at the contents too — the standard’s words are “present and not an empty string (regardless of its value)”. That is why NO_COLOR= still colours and NO_COLOR=0 switches colour off: the value does not matter, only that there is one. Read the variable once at startup and pass the flag down; checking it inside paint re-reads the environment on every call.
When to reach for a crate
Section titled “When to reach for a crate”Two sequences and a flag is all most programs need. A crate earns its place when styles have to compose (bold red on black), when 256-colour and truecolor values matter, when the console is Windows, or when the output might not be a terminal at all. owo-colors, colored and termcolor are the names you’ll meet. Whether the output is a terminal is the next step in this chapter.
- How to exit with a status code — the chapter’s previous step
- Colour, tables, and readable output — the same job in a real CLI, with
owo-colors - Back to Building a command-line tool