tty
TTY and ANSI color-support detection for Gleam. Works on both Erlang and JavaScript targets.
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):
NO_COLORset to any non-empty value →NoColorFORCE_COLORset (overrides the TTY check) →0/false=NoColor,2=Ansi256,3=TrueColor,""/1/true/unknown=Basic(case-insensitive)- Stream is not a TTY →
NoColor TERM=dumb→NoColorCOLORTERMistruecoloror24bit(case-insensitive) →TrueColorTERMcontains256→Ansi256CIset →Basic- 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:
TERM=dumbis matched exactly (dumb).TERMcontaining256yieldsAnsi256.CIis treated as enabled if the variable is set to any value.
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):
0–6or8→Dark7or9–15→Light- unset, malformed, or out of range →
Unknown
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:
is_ttydetect_color_levelcolor_level_at_leastcolor_level_comparedetect_backgroundStreamColorLevelBackground
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:
- Erlang target: OTP ≥ 26
- JavaScript target: Node ≥ 20 (or compatible runtime with
process.*)
In JavaScript runtimes without process/process.env (for example, browser
or Worker contexts), this library degrades safely:
is_tty(_)returnsFalse- env vars are treated as unset
detect_color_level(_)resolves toNoColordetect_background(_)resolves toUnknown
Requirements
- Gleam ≥ 1.11
- Erlang/OTP and Node versions tested in CI:
- OTP (Erlang target): 26, 27, 28
- Node (JavaScript target): 20, 22, 24
Troubleshooting
- Getting
NoColorunexpectedly: checkNO_COLOR, whether the stream is a TTY, and whetherTERMis set todumb. - Need color in CI/non-TTY contexts: set
FORCE_COLOR=1,2, or3. - Expected CI color but got none: ensure
CIis actually set in your environment.
For contributor setup, testing, and release workflow details, see DEV.md.
License
Dual licensed under either of
- MIT License (LICENSE-MIT)
- Apache License, Version 2.0 (LICENSE-APACHE)
at your option.