Skip to main content

src/spruce.gleam

//// spruce — a terminal-UI kit for Gleam.
////
//// spruce renders styled terminal output — colors, boxes, semantic message
//// lines, icons, deterministic hash-colors, ANSI-aware alignment, and
//// grouped/indented output — that automatically respects the terminal's color
//// support. It is logging-agnostic and targets both Erlang and JavaScript.
////
//// ## The `Spruce` context
////
//// Every render function takes an explicit `Spruce` value. The context carries
//// two things: the detected color level (so output is plain when color is
//// unsupported) and the current indent depth (so grouped output nests without
//// any global state).
////
//// ```gleam
//// import spruce
////
//// pub fn main() {
////   let sp = spruce.detect()
////   // pass `sp` to render functions in spruce/style, spruce/box, etc.
////   echo spruce.supports_color(sp)
//// }
//// ```
////
//// Use `spruce.no_color()` in tests to get deterministic, escape-free output.

import tty

/// The terminal color support level, re-exported from the `tty` package.
/// One of `NoColor`, `Basic`, `Ansi256`, or `TrueColor`.
pub type ColorLevel =
  tty.ColorLevel

/// An output stream, re-exported from the `tty` package.
/// One of `Stdin`, `Stdout`, or `Stderr`.
pub type Stream =
  tty.Stream

/// The terminal background, re-exported from the `tty` package.
/// One of `Light`, `Dark`, or `Unknown`. Adaptive colors (see
/// `spruce/style.Adaptive`) resolve against this, treating `Unknown` as `Dark`.
pub type Background =
  tty.Background

/// The rendering context. Carries the detected color level, the detected
/// terminal background, and the current indent depth. Construct it with
/// `detect`, `with_color_level`, or `no_color`, and deepen it with `indented`.
pub opaque type Spruce {
  Spruce(color: tty.ColorLevel, background: tty.Background, depth: Int)
}

/// Build a context by auto-detecting the color support of standard output.
///
/// Detection honors `NO_COLOR`, `FORCE_COLOR`, `TERM`, CI environment hints,
/// and TTY status (via the `tty` package), falling back to `NoColor` when
/// uncertain.
pub fn detect() -> Spruce {
  Spruce(
    color: tty.detect_color_level(tty.Stdout),
    background: tty.detect_background(tty.Stdout),
    depth: 0,
  )
}

/// Build a context by auto-detecting the color support of a specific stream.
pub fn detect_stream(stream: Stream) -> Spruce {
  Spruce(
    color: tty.detect_color_level(stream),
    background: tty.detect_background(stream),
    depth: 0,
  )
}

/// Build a context with an explicit color level, bypassing detection.
/// Useful for forcing a level in tests or honoring a user `--color` flag.
/// The background defaults to `Unknown` (treated as `Dark` by adaptive colors);
/// override it with `with_background`.
pub fn with_color_level(level: ColorLevel) -> Spruce {
  Spruce(color: level, background: tty.Unknown, depth: 0)
}

/// Build a context that never emits color. All output is plain text.
/// This is the recommended context for deterministic tests.
pub fn no_color() -> Spruce {
  Spruce(color: tty.NoColor, background: tty.Unknown, depth: 0)
}

/// Get the color level of a context.
pub fn color_level(sp: Spruce) -> ColorLevel {
  sp.color
}

/// Whether the context will emit any color (i.e. its level is not `NoColor`).
pub fn supports_color(sp: Spruce) -> Bool {
  sp.color != tty.NoColor
}

/// Get the detected terminal background of a context.
pub fn background(sp: Spruce) -> Background {
  sp.background
}

/// Return a copy of the context with an explicit terminal background, bypassing
/// detection. Useful for forcing light/dark adaptive colors in tests or honoring
/// a user `--background` flag.
pub fn with_background(sp: Spruce, background: Background) -> Spruce {
  Spruce(..sp, background:)
}

/// Get the current indent depth of a context (0 at the top level).
pub fn depth(sp: Spruce) -> Int {
  sp.depth
}

/// Return a copy of the context with its indent depth increased by one.
/// `spruce/group` uses this to hand a deeper context to grouped bodies.
pub fn indented(sp: Spruce) -> Spruce {
  Spruce(..sp, depth: sp.depth + 1)
}