tty

TTY and ANSI color-support detection for Gleam. Works on both Erlang and JavaScript targets.

Hex Docs

Install

gleam add tty

Usage

import gleam/io
import tty.{Ansi256, Basic, NoColor, Stdout, TrueColor}

pub fn main() {
  case tty.is_tty(Stdout) {
    True -> io.println("interactive!")
    False -> io.println("piped or redirected")
  }

  case tty.detect_color_level(Stdout) {
    NoColor -> io.println("plain text")
    Basic -> io.println("16 colors")
    Ansi256 -> io.println("256 colors")
    TrueColor -> io.println("24-bit color")
  }
}

These examples use Stdout, but CLIs that write data on stdout and diagnostics on stderr usually want to check Stderr instead — e.g. tty.is_tty(Stderr) to decide whether to color a progress bar or log output while stdout is being piped elsewhere.

Color resolution rules

detect_color_level(stream) evaluates in order (first match wins):

  1. NO_COLOR set to any non-empty value → NoColor
  2. FORCE_COLOR set (overrides the TTY check) → 0/false=NoColor, 2=Ansi256, 3=TrueColor, ""/1/true/unknown=Basic (case-insensitive)
  3. Stream is not a TTY → NoColor
  4. TERM=dumbNoColor
  5. COLORTERM is truecolor or 24bit (case-insensitive) → TrueColor
  6. TERM contains 256Ansi256
  7. CI set → Basic
  8. Default (unknown TTY with no color hints) → NoColor

The default in rule 8 errs on the side of safety: emitting ANSI escapes to a terminal that may not handle them is worse than rendering plain text. Set FORCE_COLOR=1 (or any other supported value) to opt in explicitly.

Honors the NO_COLOR standard and uses a precedence model inspired by chalk/supports-color.

Notes:

Background detection

detect_background(stream) reports whether the terminal has a Light or Dark background, or Unknown when it cannot tell. It reads the COLORFGBG environment variable (set by terminals such as rxvt and konsole) and inspects the background palette index (the second ;-separated field):

Detection is conservative: it only reports Light/Dark on a clear signal and never guesses from TERM. Most terminals do not set COLORFGBG, so Unknown is common — consumers should apply their own default (a common choice is to treat Unknown as Dark) or accept an explicit override.

import tty.{Dark, Light, Stdout, Unknown, detect_background}

case detect_background(Stdout) {
  Light -> render_for_light_background()
  Dark -> render_for_dark_background()
  Unknown -> render_default_theme()
}

Public API guidance

For 1.x, this module intentionally exposes a small stable surface:

ColorLevel is intentionally closed for 1.x (NoColor, Basic, Ansi256, TrueColor) to keep matching behavior predictable. Compare levels with color_level_at_least or color_level_compare rather than relying on any numeric rank; the integer mapping is an internal implementation detail.

Background is likewise closed for 1.x (Light, Dark, Unknown).

You can also gate behavior on the detected level without matching every variant:

import tty.{Ansi256, Stdout, color_level_at_least, detect_color_level}

let level = detect_color_level(Stdout)
case color_level_at_least(actual: level, at_least: Ansi256) {
  True -> render_256_color()
  False -> render_basic()
}

For finer-grained ordering, color_level_compare returns a gleam/order Order, so it composes with list.sort, order.reverse, and friends:

import gleam/order
import tty.{Ansi256, Basic, color_level_compare}

color_level_compare(Basic, Ansi256)
// -> order.Lt

Runtime compatibility and fallback behavior

Supported runtimes:

In JavaScript runtimes without process/process.env (for example, browser or Worker contexts), this library degrades safely:

Requirements

Troubleshooting

For contributor setup, testing, and release workflow details, see DEV.md.

License

Dual licensed under either of

at your option.

Search Document