Skip to main content

src/tty_ffi.erl

-module(tty_ffi).
-export([stdin_is_tty/0, stdout_is_tty/0, stderr_is_tty/0, get_env/1]).
%% Exported for contract testing only; not used from Gleam.
-export([env_value_to_result/1]).

stdin_is_tty()  -> tty_option_enabled(standard_io, stdin).
stdout_is_tty() -> tty_option_enabled(standard_io, stdout).
stderr_is_tty() -> tty_option_enabled(standard_io, stderr).

%% io:getopts/1 is available on OTP 26+. On OTP 26+ the standard_io device
%% reports per-stream TTY status via the stdin/stdout/stderr keys; the
%% standard_error device does NOT carry those keys, so all three queries
%% target standard_io. Any failure to read options (missing function on older
%% OTP, badarg, a closed/terminated I/O server at shutdown, etc.) falls back to
%% false so Gleam's Bool result is total. The catch-all is deliberate: the
%% public contract is "returns false when TTY status cannot be determined", so
%% no exception class should ever escape this function.
tty_option_enabled(Dev, Key) ->
    try io:getopts(Dev) of
        Opts when is_list(Opts) ->
            proplists:get_value(Key, Opts, false) =:= true;
        _ ->
            false
    catch
        _:_ ->
            false
    end.

%% Returns {ok, Value} | {error, nil} to match Gleam's Result(String, Nil).
get_env(Name) when is_binary(Name) ->
    case unicode:characters_to_list(Name) of
        EnvName when is_list(EnvName) ->
            case os:getenv(EnvName) of
                false -> {error, nil};
                Value -> env_value_to_result(Value)
            end;
        {error, _, _} ->
            {error, nil}
    end.

env_value_to_result(Value) ->
    case unicode:characters_to_binary(Value) of
        EnvValue when is_binary(EnvValue) -> {ok, EnvValue};
        {error, _, _} -> {error, nil}
    end.