Managing colors in Rust
There are many Rust libraries for managing terminal colors. You should use owo-colors because it is the only library I've found that meets all of these criteria:
- actively maintained
- has a simple, intuitive API
- minimizes dependencies on global state
- involves zero allocations
Note: you should not use termcolor because it targets the deprecated Console APIs on Windows—and has a significantly more complicated API as a result.
Instead, you should use a library that just only supports ANSI color codes, and initialize support for them on Windows with enable-ansi-support.
There are two general ways with which color support can be handled. I'm going to call them the "immediate pattern" and the "stylesheet approach", respectively. Library code that supports colors should use the stylesheet approach. Code in a binary crate can use whichever pattern leads to simpler code.
The immediate pattern
This pattern is usually presented in examples and tutorials. It is conceptually quite simple.
Here's an example of what it looks like:
#![allow(dead_code)]
// The owo-colors "supports-colors" feature must be enabled.
use clap::{ArgEnum, Parser};
use owo_colors::{OwoColorize, Stream};
#[derive(Debug, Parser)]
struct MyApp {
#[clap(long, arg_enum, global = true, default_value = "auto")]
color: Color,
}
#[derive(ArgEnum, Clone, Copy, Debug)]
enum Color {
Always,
Auto,
Never,
}
impl Color {
fn init(self) {
// Set a supports-color override based on the variable passed in.
match self {
Color::Always => owo_colors::set_override(true),
Color::Auto => {}
Color::Never => owo_colors::set_override(false),
}
}
}
fn main() {
let app = MyApp::parse();
app.color.init();
println!(
"My number is {}",
42.if_supports_color(Stream::Stdout, |text| text.bright_blue())
);
}
Notes:
owo_colors::set_override
is used to control color support globally. The global configuration only has an effect ifif_supports_color
is called.println!
is paired withStream::Stdout
. If this wereeprintln!
, it would need to be paired withStream::Stderr
.
While this pattern is sometimes convenient in binary code, it should not be used in libraries. That is because libraries should not print information directly out to stdout or stderr—instead, they should return values that implement Display
or similar. Library code should use the stylesheet approach instead.
The stylesheet approach
This pattern involves defining a Styles
struct containing colors and styles to apply to a text.
A stylesheet is simply a list of dynamic styles, customized to a particular type to be displayed. Here's an example:
use std::fmt;
use owo_colors::{OwoColorize, Style};
// Stylesheet used to colorize MyValueDisplay below.
#[derive(Debug, Default)]
struct Styles {
number_style: Style,
shape_style: Style,
// ... other styles
}
impl Styles {
fn colorize(&mut self) {
self.number_style = Style::new().bright_blue();
self.shape_style = Style::new().bright_green();
// ... other styles
}
}
#[derive(Debug)]
pub struct MyValue {
number: usize,
shape: &'static str,
}
impl MyValue {
pub fn new(number: usize, shape: &'static str) -> Self {
Self { number, shape }
}
/// Returns a type that can display `MyValue`.
pub fn display(&self) -> MyValueDisplay<'_> {
MyValueDisplay {
value: self,
styles: Box::default(),
}
}
}
/// Displayer for [`MyValue`].
pub struct MyValueDisplay<'a> {
value: &'a MyValue,
styles: Box<Styles>,
}
impl<'a> MyValueDisplay<'a> {
/// Colorizes the output.
pub fn colorize(&mut self) {
self.styles.colorize();
}
}
impl<'a> fmt::Display for MyValueDisplay<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"My number is {}, and my shape is a {}",
self.value.number.style(self.styles.number_style),
self.value.shape.style(self.styles.shape_style),
)
}
}
Here's some library code that uses the above stylesheet:
use std::fmt;
use owo_colors::{OwoColorize, Style};
// Stylesheet used to colorize MyValueDisplay below.
#[derive(Debug, Default)]
struct Styles {
number_style: Style,
shape_style: Style,
// ... other styles
}
impl Styles {
fn colorize(&mut self) {
self.number_style = Style::new().bright_blue();
self.shape_style = Style::new().bright_green();
// ... other styles
}
}
#[derive(Debug)]
pub struct MyValue {
number: usize,
shape: &'static str,
}
impl MyValue {
pub fn new(number: usize, shape: &'static str) -> Self {
Self { number, shape }
}
/// Returns a type that can display `MyValue`.
pub fn display(&self) -> MyValueDisplay<'_> {
MyValueDisplay {
value: self,
styles: Box::default(),
}
}
}
/// Displayer for [`MyValue`].
pub struct MyValueDisplay<'a> {
value: &'a MyValue,
styles: Box<Styles>,
}
impl<'a> MyValueDisplay<'a> {
/// Colorizes the output.
pub fn colorize(&mut self) {
self.styles.colorize();
}
}
impl<'a> fmt::Display for MyValueDisplay<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"My number is {}, and my shape is a {}",
self.value.number.style(self.styles.number_style),
self.value.shape.style(self.styles.shape_style),
)
}
}
And finally, here's the binary code that uses the library.
#![allow(dead_code)]
use clap::{ArgEnum, Parser};
use my_app::MyValue;
use owo_colors::Stream;
#[derive(Debug, Parser)]
struct MyApp {
#[clap(long, arg_enum, global = true, default_value = "auto")]
color: Color,
}
#[derive(ArgEnum, Clone, Copy, Debug)]
enum Color {
Always,
Auto,
Never,
}
// This example uses the supports-color crate:
// https://crates.io/crates/supports-color
//
// MyApp and Color definitions are repeated from the "immediate pattern"
// example above.
impl Color {
fn supports_color_on(self, stream: Stream) -> bool {
match self {
Color::Always => true,
Color::Auto => supports_color::on_cached(stream).is_some(),
Color::Never => false,
}
}
}
fn main() {
let app = MyApp::parse();
let my_value = MyValue::new(24, "circle");
let mut display = my_value.display();
if app.color.supports_color_on(Stream::Stdout) {
display.colorize();
}
println!("{}", display);
}
Notes:
- Library code is completely unaware of whether the environment supports colors. All it cares about is whether the
colorize
method is called.- Note that the global
set_override
andunset_override
methods have no impact on library code in the stylesheet example. - The global methods are only active if
if_supports_color
is called, as shown by the example for the immediate pattern above. This is by design: most libraries shouldn't reach out to global state.
- Note that the global
- The stylesheet is stored as
Box<Styles>
. The boxing isn't strictly required, but eachStyle
is pretty large, and a struct containing e.g. 16 styles is 272 bytes as of owo-colors 3.2.0. That's a pretty large amount of data to store on the stack. Styles::default()
initializes all the styles to having no effect. Thecolorize()
method then initializes them as required.- For custom color support,
Styles
can be made public. Most library code won't need to give users the ability to customize styles, but this pattern naturally extends to that use case. - Use of a separate
MyAppDisplay
type. Thecolorize
call is isolated to this particularMyAppDisplay
, without influencing other display calls. println!
is paired withStream::Stdout
. If this wereeprintln!
, it would need to be paired withStream::Stderr
.