src/ansi.erl

-module(ansi).

% API
-export([style/1]).
-export([style/2]).
-export([clear/1]).
-export([cursor/1]).
-export([screen/1]).
-export([terminal/1]).
-export([detect_mode/0]).

-on_load(on_load/0).

-define(format(N, Format, Code),
format(Mode, Format) when Mode >= N -> Code;
format(_Mode, Format) -> []
).

%--- API -----------------------------------------------------------------------

style(Format) ->
    case persistent_term:get(ansi_term, undefined) of
        no_color ->
            [];
        Mode ->
            case sequence(Mode, Format) of
                [] -> [];
                Sequence -> [control_sequence(Sequence), $m]
            end
    end.

style(Format, IOData) -> [style(Format), IOData, style(reset)].

clear(Format) -> control_sequence(clear_(Format)).

clear_(display) -> $J;
clear_(to_end_of_screen) -> "0J";
clear_(to_beginning_of_screen) -> "1J";
clear_(screen) -> "2J";
clear_(saved_lines) -> "3J";
clear_(in_line) -> $K;
clear_(end_of_line) -> "0K";
clear_(beginning_of_line) -> "1K";
clear_(line) -> "2K";
clear_(Format) -> error({invalid_erase, Format}).

cursor(Format) -> control_sequence(cursor_(Format)).

cursor_(home) ->
    $H;
cursor_({Direction, 0}) when
    Direction == up; Direction == down; Direction == left; Direction == right
->
    [];
cursor_({up, Lines}) when Lines > 0 ->
    [integer_to_list(Lines), $A];
cursor_({up, Lines}) when Lines < 0 ->
    cursor_({down, abs(Lines)});
cursor_({down, Lines}) when Lines > 0 ->
    [integer_to_list(Lines), $B];
cursor_({down, Lines}) when Lines < 0 ->
    cursor_({up, abs(Lines)});
cursor_({left, Lines}) when Lines > 0 ->
    [integer_to_list(Lines), $C];
cursor_({left, Lines}) when Lines < 0 ->
    cursor_({right, abs(Lines)});
cursor_({right, Lines}) when Lines > 0 ->
    [integer_to_list(Lines), $D];
cursor_({right, Lines}) when Lines < 0 ->
    cursor_({left, abs(Lines)});
cursor_({Direction, beginning, 0}) when Direction == up; Direction == down ->
    [];
cursor_({down, beginning, Lines}) when is_integer(Lines), Lines > 0 ->
    [integer_to_list(Lines), $E];
cursor_({down, beginning, Lines}) when Lines < 0 ->
    cursor_({up, beginning, abs(Lines)});
cursor_({up, beginning, Lines}) when is_integer(Lines), Lines > 0 ->
    [integer_to_list(Lines), $F];
cursor_({up, beginning, Lines}) when Lines < 0 ->
    cursor_({down, beginning, abs(Lines)});
cursor_({column, Column}) when is_integer(Column), Column > 0 ->
    [integer_to_list(Column), $G];
cursor_({line, Line}) when is_integer(Line), Line > 0 ->
    [integer_to_list(Line), $d];
cursor_({Line, Column}) when
    is_integer(Line), is_integer(Column), Line > 0, Column > 0
->
    [integer_to_list(Line), $;, integer_to_list(Column), $H];
cursor_(up_scroll) ->
    $M;
cursor_(save) ->
    $s;
cursor_(restore) ->
    $u;
cursor_(invisible) ->
    "?25l";
cursor_(visible) ->
    "?25h";
cursor_(Format) ->
    error({invalid_cursor, Format}).

screen(Format) -> control_sequence(screen_(Format)).

screen_(save) ->
    "?47h";
screen_(restore) ->
    "?47l";
screen_({alternate_buffer, enable}) ->
    "?1049h";
screen_({alternate_buffer, disable}) ->
    "?1049l";
screen_({scrolling_region, Top, Bottom}) when Top >= 1 ->
    [integer_to_list(Top), $;, integer_to_list(Bottom), $r];
screen_({scrolling_region, reset}) ->
    $r;
screen_(Format) ->
    error({invalid_screen, Format}).

terminal(height) ->
    {ok, Rows} = io:rows(),
    Rows;
terminal(width) ->
    {ok, Columns} = io:columns(),
    Columns;
terminal(mode) ->
    persistent_term:get(ansi_term, undefined);
terminal({mode, Mode}) when
    Mode =:= 8; Mode =:= 16; Mode =:= 256; Mode == true; Mode == no_color
->
    persistent_term:put(ansi_term, Mode);
terminal({mode, Mode}) ->
    error({invalid_mode, Mode}).

% @private
detect_mode() ->
    case terminal(mode) of
        undefined ->
            Mode =
                case os:getenv("NO_COLOR") of
                    false ->
                        case os:getenv("COLORTERM") of
                            "truecolor" ->
                                true;
                            _Else ->
                                case tput_colors() of
                                    N when N =:= 8; N =:= 16; N =:= 256 -> N;
                                    N when N > 256 -> true;
                                    false -> term_color(os:getenv("TERM"))
                                end
                        end;
                    _ ->
                        no_color
                end,
            terminal({mode, Mode});
        _Mode ->
            ok
    end.

%--- Callbacks -----------------------------------------------------------------

on_load() -> detect_mode().

%--- Internal ------------------------------------------------------------------

control_sequence(Sequence) -> [$\e, $[, Sequence].

sequence(Mode, [Format]) ->
    sequence(Mode, Format);
sequence(Mode, [Format | Rest]) ->
    case format(Mode, Format) of
        [] -> sequence(Mode, Rest);
        Code -> [Code, $;, sequence(Mode, Rest)]
    end;
sequence(Mode, Format) ->
    format(Mode, Format).

% Modes
?format(8, bold, "1");
?format(8, dim, "2");
?format(8, faint, "2");
?format(8, italic, "3");
?format(8, underline, "4");
?format(8, blinking, "5");
?format(8, inverse, "7");
?format(8, reverse, "7");
?format(8, hidden, "8");
?format(8, invisible, "8");
?format(8, strikethrough, "9");
?format(8, reset_bold, "21");
?format(8, reset_dim, "22");
?format(8, reset_faint, "22");
?format(8, reset_italic, "23");
?format(8, reset_underline, "24");
?format(8, reset_blinking, "25");
?format(8, reset_inverse, "27");
?format(8, reset_reverse, "27");
?format(8, reset_hidden, "28");
?format(8, reset_invisible, "28");
?format(8, reset_strikethrough, "29");
?format(8, reset, $0);
% Foreground
?format(8, black, "30");
?format(8, red, "31");
?format(8, green, "32");
?format(8, yellow, "33");
?format(8, blue, "34");
?format(8, magenta, "35");
?format(8, cyan, "36");
?format(8, white, "37");
?format(8, default, "39");
% Background
?format(8, bg_black, "40");
?format(8, bg_red, "41");
?format(8, bg_green, "42");
?format(8, bg_yellow, "43");
?format(8, bg_blue, "44");
?format(8, bg_magenta, "45");
?format(8, bg_cyan, "46");
?format(8, bg_white, "47");
?format(8, bg_default, "49");
?format(16, bright_black, "90");
?format(16, bg_bright_black, "100");
?format(16, bright_red, "91");
?format(16, bg_bright_red, "101");
?format(16, bright_green, "92");
?format(16, bg_bright_green, "102");
?format(16, bright_yellow, "93");
?format(16, bg_bright_yellow, "103");
?format(16, bright_blue, "94");
?format(16, bg_bright_blue, "104");
?format(16, bright_magenta, "95");
?format(16, bg_bright_magenta, "105");
?format(16, bright_cyan, "96");
?format(16, bg_bright_cyan, "106");
?format(16, bright_white, "97");
?format(16, bg_bright_white, "107");
format(Mode, {fg, N}) when Mode >= 256, N =< 255 ->
    ["38;5;", integer_to_list(N)];
format(_Mode, {fg, N}) when N =< 255 ->
    [];
format(Mode, {bg, N}) when Mode >= 256, N =< 255 ->
    ["48;5;", integer_to_list(N)];
format(_Mode, {bg, N}) when N =< 255 ->
    [];
format(Mode, {fg, {R, G, B}}) when Mode >= true, R =< 255, G =< 255, B =< 255 ->
    [
        "38;2;",
        integer_to_list(R),
        $;,
        integer_to_list(G),
        $;,
        integer_to_list(B)
    ];
format(_Mode, {fg, {R, G, B}}) when R =< 255, G =< 255, B =< 255 ->
    [];
format(Mode, {bg, {R, G, B}}) when Mode >= true, R =< 255, G =< 255, B =< 255 ->
    [
        "48;2;",
        integer_to_list(R),
        $;,
        integer_to_list(G),
        $;,
        integer_to_list(B)
    ];
format(_Mode, {bg, {R, G, B}}) when R =< 255, G =< 255, B =< 255 ->
    [];
format(_Mode, bel) ->
    7;
format(_Mode, Format) ->
    error({invalid_style, Format}).

tput_colors() ->
    case os:find_executable("tput") of
        false ->
            false;
        _Tput ->
            case string:to_integer(string:trim(os:cmd("tput colors"))) of
                {Int, _Rest} -> Int;
                _Error -> false
            end
    end.

term_color("screen-256color") ->
    color_256;
term_color("xterm-kitty") ->
    color_256;
term_color(_Term) ->
    io:format("TERM: ~p~n", [_Term]),
    no_color.