-module(tty). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/tty.gleam"). -export([color_level_compare/2, color_level_at_least/2, is_tty/1, detect_color_level/1, detect_background/1]). -export_type([stream/0, color_level/0, background/0]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. ?MODULEDOC( " TTY and ANSI color-support detection.\n" "\n" " This module answers two questions a CLI program asks at startup:\n" " 1. Is this stream connected to a terminal? (`is_tty`)\n" " 2. What level of ANSI color does it support? (`detect_color_level`)\n" ). -type stream() :: stdin | stdout | stderr. -type color_level() :: no_color | basic | ansi256 | true_color. -type background() :: light | dark | unknown. -file("src/tty.gleam", 103). ?DOC( " Internal capability rank for a `ColorLevel` (`NoColor`=0 .. `TrueColor`=3).\n" " The numeric values are an implementation detail, not part of the public\n" " 1.x API — callers should use `color_level_compare`/`color_level_at_least`.\n" ). -spec color_level_rank(color_level()) -> integer(). color_level_rank(Level) -> case Level of no_color -> 0; basic -> 1; ansi256 -> 2; true_color -> 3 end. -file("src/tty.gleam", 96). ?DOC( " Orders two color levels by capability, where\n" " `NoColor` < `Basic` < `Ansi256` < `TrueColor`. Returns a `gleam/order`\n" " `Order`, so it composes with `list.sort`, `order.reverse`, and friends.\n" "\n" " ```gleam\n" " tty.color_level_compare(Basic, Ansi256)\n" " // -> order.Lt\n" " ```\n" ). -spec color_level_compare(color_level(), color_level()) -> gleam@order:order(). color_level_compare(A, B) -> gleam@int:compare(color_level_rank(A), color_level_rank(B)). -file("src/tty.gleam", 78). ?DOC( " Returns `True` if the actual color level is at least as capable as the\n" " required level. Use this to gate features without matching every variant:\n" "\n" " ```gleam\n" " let level = tty.detect_color_level(Stdout)\n" " case tty.color_level_at_least(actual: level, at_least: Ansi256) {\n" " True -> render_256_color()\n" " False -> render_basic()\n" " }\n" " ```\n" ). -spec color_level_at_least(color_level(), color_level()) -> boolean(). color_level_at_least(Actual, Required) -> case color_level_compare(Actual, Required) of lt -> false; eq -> true; gt -> true end. -file("src/tty.gleam", 115). ?DOC( " Inverse of `color_level_rank`: maps a `0..3` rank back to a `ColorLevel`,\n" " returning `Error(Nil)` for any out-of-range value. Used to convert the\n" " internal resolver's rank into a `ColorLevel`.\n" ). -spec color_level_from_rank(integer()) -> {ok, color_level()} | {error, nil}. color_level_from_rank(Rank) -> case Rank of 0 -> {ok, no_color}; 1 -> {ok, basic}; 2 -> {ok, ansi256}; 3 -> {ok, true_color}; _ -> {error, nil} end. -file("src/tty.gleam", 128). ?DOC( " Inverse of the internal background rank: maps a `0..2` rank back to a\n" " `Background`, returning `Error(Nil)` for any out-of-range value. Used to\n" " convert the internal resolver's rank into a `Background`.\n" ). -spec background_from_rank(integer()) -> {ok, background()} | {error, nil}. background_from_rank(Rank) -> case Rank of 0 -> {ok, unknown}; 1 -> {ok, dark}; 2 -> {ok, light}; _ -> {error, nil} end. -file("src/tty.gleam", 151). ?DOC( " Returns `True` if the given stream is connected to a terminal.\n" "\n" " On the Erlang target this uses `io:getopts/1` (requires OTP 26+). If\n" " terminal options cannot be read, this returns `False`.\n" " On the JavaScript target this uses `process.stdin.isTTY`,\n" " `process.stdout.isTTY`, or `process.stderr.isTTY`, so it requires a\n" " Node-style runtime with those streams.\n" "\n" " ```gleam\n" " case tty.is_tty(Stdout) {\n" " True -> show_spinner()\n" " False -> print_plain_progress()\n" " }\n" " ```\n" ). -spec is_tty(stream()) -> boolean(). is_tty(Stream) -> case Stream of stdin -> tty_ffi:stdin_is_tty(); stdout -> tty_ffi:stdout_is_tty(); stderr -> tty_ffi:stderr_is_tty() end. -file("src/tty.gleam", 242). -spec get_env(binary()) -> {ok, binary()} | {error, nil}. get_env(Name) -> tty_ffi:get_env(Name). -file("src/tty.gleam", 177). ?DOC( " Detects color support for a stream, honoring `NO_COLOR`, `FORCE_COLOR`,\n" " `CI`, `TERM`, and `COLORTERM` environment variables.\n" "\n" " On the JavaScript target this reads `process.env`, so it requires a\n" " Node-style runtime.\n" "\n" " When a JavaScript runtime does not provide `process` or `process.env`,\n" " environment variables are treated as unset and this function falls back to\n" " `NoColor` unless other forced inputs are available.\n" "\n" " ```gleam\n" " case tty.detect_color_level(Stdout) {\n" " NoColor -> render_without_ansi()\n" " Basic -> render_with_basic_ansi()\n" " Ansi256 -> render_with_256_colors()\n" " TrueColor -> render_with_truecolor()\n" " }\n" " ```\n" ). -spec detect_color_level(stream()) -> color_level(). detect_color_level(Stream) -> Rank = tty@resolve_color_level:resolve_color_level( is_tty(Stream), fun get_env/1 ), Level@1 = case color_level_from_rank(Rank) of {ok, Level} -> Level; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"resolve_color_level must return a rank in 0..3"/utf8>>, file => <>, module => <<"tty"/utf8>>, function => <<"detect_color_level"/utf8>>, line => 183, value => _assert_fail, start => 5703, 'end' => 5753, pattern_start => 5714, pattern_end => 5723}) end, Level@1. -file("src/tty.gleam", 209). ?DOC( " Detects whether the terminal background is light or dark from `COLORFGBG`.\n" "\n" " The `stream` argument is accepted for API symmetry with `detect_color_level`\n" " and to leave room for future stream-specific detection; the current\n" " `COLORFGBG` signal is environment-wide and stream-independent.\n" "\n" " On the JavaScript target this reads `process.env`, so it requires a\n" " Node-style runtime. When a JavaScript runtime does not provide `process` or\n" " `process.env`, environment variables are treated as unset and this function\n" " returns `Unknown`.\n" "\n" " Returns `Unknown` when `COLORFGBG` is unset, malformed, or does not contain a\n" " clear background palette index.\n" "\n" " ```gleam\n" " case tty.detect_background(Stdout) {\n" " Light -> render_dark_text()\n" " Dark -> render_light_text()\n" " Unknown -> render_default_theme()\n" " }\n" " ```\n" ). -spec detect_background(stream()) -> background(). detect_background(Stream) -> _ = Stream, Rank = tty@resolve_background:resolve_background(fun get_env/1), Background@1 = case background_from_rank(Rank) of {ok, Background} -> Background; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"resolve_background must return a rank in 0..2"/utf8>>, file => <>, module => <<"tty"/utf8>>, function => <<"detect_background"/utf8>>, line => 216, value => _assert_fail, start => 7180, 'end' => 7234, pattern_start => 7191, pattern_end => 7205}) end, Background@1.