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")
}
}
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.
Public API guidance
For 1.x, this module intentionally exposes a small stable surface:
is_ttydetect_color_levelcolor_level_at_leastcolor_level_compareStreamColorLevel
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.
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()
}
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 toNoColor
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.