Adjust your application for a light or dark terminal

6 minute read Published: 2022-07-16

So you want your unix application to select different sets of colors depending on whether the terminal is set to a light or dark theme ?

Here's an how-to. It's first targeted at Rust programmers but each step can be ported to other languages.

Here's broot, an example of what you may want to achieve.

In a dark terminal:

broot dark

In a light terminal:

broot light

broot is open-source but is big and complex, so we'll look at the basics with a shorter example.

Before we start, just let's check you need all this. Maybe you don't need to detect whether the terminal is dark or light ? Most applications can reach honorable results with one of those three simple solutions:

If you know those solutions aren't up to your challenge, this how-to is for you.

The two basic parts of light dependent styling are

  1. Detect whether the terminal is light or dark
  2. Use dynamic colors

Detect whether the terminal is dark or light

The best solution seems to be to issue the "dynamic colors" OSC escape sequence and check the result.

This is an xterm extension which works on all unix/mac terminals (at least all the ones the users of my applications tried).

If you're coding in Rust, I suggest you use my terminal-light crate, because:

If you're not making a Rust application, steal... err... port the source to your favourite language.

This crate gives you the terminal's background either as RGB or as luma, a number between 0 and 1 which is the best to determine whether it's dark (luma is small) or light (luma is big).

Here's an example from Clima:

fn make_skin() -> MadSkin {
    match terminal_light::luma() {
        Ok(luma) if luma > 0.6 => MadSkin::default_light(),
        Ok(_) => MadSkin::default_dark(),
        Err(_) => MadSkin::default(), // this skin works in both light and dark
    }
}

This function returns a termimad MadSkin set of colors (you don't have to use termimad, more on this later): the default termimad light skin when the luma is high, the default dark skin when it's low, and a medium default skin when we don't know.

Note: clima is a small markdown viewer, not one you should use because its use-case is super niche, but one which you may want to have a look at if if you want to see how to deal with display or managing key inputs in a tiny TUI application.

Use dynamic colors

Popular terminal styling libraries often optimize for code like this:

print!("hey!".red());

You don't want that if your goal is to adjust or configure colors.

Some libraries only allow hard-coded colors, some let you deal with dynamic ones. I personally like Crossterm which among other features is cross-platform, meaning it supports also Windows terminals (not the very old ones, though).

We'll define a skin with 3 styles with Crossterm:

use crossterm::style::{Color, ContentStyle};

struct Skin {
    high_contrast: ContentStyle,
    low_contrast: ContentStyle,
    code: ContentStyle,
}

fn main() {
    let skin = match terminal_light::luma() {
        Ok(luma) if luma > 0.6 => Skin { // light theme
            high_contrast: ContentStyle {
                foreground_color: Some(Color::Rgb { r: 40, g: 5, b: 0 }),
                .. Default::default()
            },
            low_contrast: ContentStyle {
                foreground_color: Some(Color::Rgb { r: 120, g: 120, b: 80 }),
                .. Default::default()
            },
            code: ContentStyle {
                foreground_color: Some(Color::Rgb { r: 50, g: 50, b: 50 }),
                background_color: Some(Color::Rgb { r: 210, g: 210, b: 210 }),
                .. Default::default()
            },
        },
        _ => Skin { // dark theme
            high_contrast: ContentStyle {
                foreground_color: Some(Color::Rgb { r: 250, g: 180, b: 0 }),
                .. Default::default()
            },
            low_contrast: ContentStyle {
                foreground_color: Some(Color::Rgb { r: 180, g: 150, b: 0 }),
                .. Default::default()
            },
            code: ContentStyle {
                foreground_color: Some(Color::Rgb { r: 220, g: 220, b: 220 }),
                background_color: Some(Color::Rgb { r: 80, g: 80, b: 80 }),
                .. Default::default()
            },
        },
    };
    println!("\n {}", skin.low_contrast.apply("This line is easy to read but low intensity"));
    println!("\n {}", skin.high_contrast.apply("This line has a much greater contrast"));
    println!("\n {}", skin.code.apply("    this.is_meant_to_be(some_code);"));
    println!();
}

Here's what you'd have on a dark terminal:

dark

And on a light terminal:

light

This program is one of the examples in the terminal-light repository.

Note: Be careful that many terminals, even in recent systems, aren't configured for full RGB colors. By default, a terminal application should use 8 bit ANSI colors and propose RGB only as opt-in. Hopefully all terminal styling libraries support 8 bit ANSI colors.

Go Further

Compute colors

Knowing the background, more color computations become possible, like fading a page, or computing a range of colors for a map or just different levels of titles.

If you stick to the ANSI range, this is a little delicate, because this range is limited, but if you check the results and proceed with care, it can give good results. I made the coolor crate for such computations.

Read skins from files

Users often want to choose the colors of the applications they use every days. A skin file is useful for that, and is easy enough to implement in Rust using serde.

You can let users (like me) using several terminals specify different skins to use for light or dark terminals. This is what I do in broot:

imports: [

        # This file contains the skin to use when the terminal
        # is dark (or when this couldn't be determined)
        {
            luma: [
                dark
                unknown
            ]
            file: dark-blue-skin.hjson
        }

        # This skin is imported when your terminal is light
        {
            luma: light
            file: white-skin.hjson
        }
]