supports-color

Detects terminal color support level (basic, 256-color, or true color) and honors the NO_COLOR standard.

Library
Cargo
v3.0.2
54stars
Apache License 2.0

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
32/100Needs Attention
Development Activity0
Maintenance20
Community40
Maturity56
Momentum12

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
72/100Good
Architecture78
Code Quality80
Innovation75
Learning Curve55

supports-color is a small Rust crate that answers one question precisely: does the current terminal support ANSI colors, and if so, how many? It inspects environment variables like FORCE_COLOR, NO_COLOR, CLICOLOR, CLICOLOR_FORCE, COLORTERM, and TERM, plus whether stdout or stderr is actually a TTY, to return a ColorLevel describing basic, 256-color, or 16-million-color (truecolor) support.

It’s a direct Rust port of the widely-used sindresorhus/supports-color npm package, bringing the same battle-tested heuristics to Rust CLI tooling. A cached variant, on_cached, memoizes the result per stream using a OnceLock for programs that check color support repeatedly without needing to react to a changing environment.

What You Get

  • Color level detection - Returns none, basic, 256-color, or true-color support for a chosen stream.
  • NO_COLOR / FORCE_COLOR support - Honors the NO_COLOR standard plus FORCE_COLOR, CLICOLOR, and CLICOLOR_FORCE overrides.
  • Cached lookups - on_cached() memoizes the result per stream for repeated checks without re-reading the environment.
  • Zero-config API - A single on(Stream) call returns an Option<ColorLevel> with no setup required.

Common Use Cases

  • CLI output formatting - A command-line tool decides whether to emit ANSI color codes before printing.
  • Logging libraries - A logger chooses plain text vs. colorized log levels based on terminal capability.
  • Progress bars and spinners - Terminal UI libraries fall back to plain text when colors aren’t supported.
  • CI/CD pipelines - Tools detect CI environments (via the is_ci crate) to disable interactive color output automatically.

Under The Hood

Architecture This is a single-module library: src/lib.rs exposes two public functions (on, on_cached), a Stream enum, and a ColorLevel struct. Detection flows through one internal function, supports_color(stream), which sequentially checks FORCE_COLOR/CLICOLOR_FORCE overrides, then NO_COLOR/TERM=dumb/non-TTY negatives, then COLORTERM/TERM truecolor patterns, then TERM 256-color patterns, and finally falls back to basic ANSI/CLICOLOR/is_ci checks — the result is translated into an Option<ColorLevel> by translate_level. There’s little separation of concerns beyond this single function because the domain is inherently a compact decision tree. on_cached wraps the same logic behind a per-stream array of OnceLock, indexed by the Stream enum cast to usize, with a macro-generated compile-time assertion (assert_stream_in_bounds!) guarding that index. If Stream ever gained a third variant, the cache array and that macro invocation would both need updating — the one real sharp edge in an otherwise minimal design.

Tech Stack A Rust crate on edition 2018 with rust-version = "1.70.0", carrying a single runtime dependency, is_ci (~1.2.0), used only as a last-resort CI heuristic. It relies on std::io::IsTerminal (stabilized in Rust 1.70) instead of any external TTY-detection crate, keeping the dependency footprint essentially zero. Build tooling is plain Cargo plus a Makefile.toml (cargo-make) and cliff.toml for changelog generation via git-cliff; it’s published to crates.io and documented on docs.rs.

Code Quality Tests live in tests/cached.rs (a multithreaded stress test of on_cached) and as inline #[cfg(test)] unit tests in src/lib.rs covering the empty-environment case, CLICOLOR/CLICOLOR_FORCE paths, and cache behavior; a TEST_LOCK: Mutex<()> guards process-env mutation across tests to avoid races, which is the correct pattern for testing global env state. Error handling is minimal because the domain rarely errors — env::var results are pattern-matched inline rather than propagated. Naming is clear and consistent (env_force_color, env_no_color, check_ansi_color). CI is present but narrow: a single GitHub Actions workflow (miri.yml) runs the test suite under Miri for undefined-behavior checks, rather than a full build/lint/test matrix; one targeted #![allow(clippy::bool_to_int_with_if)] suggests clippy is run at least locally.

API Design The public surface is deliberately tiny: call on(Stream::Stdout) or on(Stream::Stderr) and get back an Option<ColorLevel> with public has_basic/has_256/has_16m booleans — no configuration, no builder, no setup. It’s a faithful port of a well-known JavaScript convention (supports-color on npm), so Rust developers coming from Node tooling get familiar semantics for free. Rustdoc comments cover every public item and the README doubles as a working example. The ColorLevel struct keeps its numeric level field private, leaving room to extend granularity later without a breaking API change. It’s solid, unsurprising ergonomics rather than a novel API — the value is in being a small, correct, dependency-light building block other terminal tooling can rely on.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search