Skip to main content

src/spruce@box.erl

-module(spruce@box).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/spruce/box.gleam").
-export([default_options/0, options/2, padding/5, margin/5, border/2, border_colors/5, border_sides/5, width/2, render/3, simple/2, print/2]).
-export_type([border_chars/0, border/0, box_options/0, box_config/0, sides/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(" Boxed terminal output with rounded borders and an optional title.\n").

-type border_chars() :: {border_chars,
        binary(),
        binary(),
        binary(),
        binary(),
        binary(),
        binary(),
        binary(),
        binary()}.

-type border() :: normal |
    rounded |
    thick |
    double |
    hidden |
    block |
    {custom, border_chars()}.

-opaque box_options() :: {box_options, binary(), spruce@style:color()} |
    {configured_box_options,
        binary(),
        spruce@style:color(),
        integer(),
        integer(),
        integer(),
        integer(),
        integer(),
        integer(),
        integer(),
        integer(),
        border(),
        gleam@option:option(integer()),
        spruce@style:color(),
        spruce@style:color(),
        spruce@style:color(),
        spruce@style:color(),
        boolean(),
        boolean(),
        boolean(),
        boolean()}.

-type box_config() :: {box_config,
        binary(),
        spruce@style:color(),
        integer(),
        integer(),
        integer(),
        integer(),
        integer(),
        integer(),
        integer(),
        integer(),
        border(),
        gleam@option:option(integer()),
        spruce@style:color(),
        spruce@style:color(),
        spruce@style:color(),
        spruce@style:color(),
        boolean(),
        boolean(),
        boolean(),
        boolean()}.

-type sides() :: {sides, boolean(), boolean(), boolean(), boolean()}.

-file("src/spruce/box.gleam", 103).
?DOC(" Default options: no title, cyan border.\n").
-spec default_options() -> box_options().
default_options() ->
    {box_options, <<""/utf8>>, cyan}.

-file("src/spruce/box.gleam", 108).
?DOC(" Build options with an explicit title and border color.\n").
-spec options(binary(), spruce@style:color()) -> box_options().
options(Title, Color) ->
    {box_options, Title, Color}.

-file("src/spruce/box.gleam", 678).
-spec non_negative(integer()) -> integer().
non_negative(Value) ->
    gleam@int:max(0, Value).

-file("src/spruce/box.gleam", 374).
-spec configured(box_config()) -> box_options().
configured(Config) ->
    {configured_box_options,
        erlang:element(2, Config),
        erlang:element(3, Config),
        erlang:element(4, Config),
        erlang:element(5, Config),
        erlang:element(6, Config),
        erlang:element(7, Config),
        erlang:element(8, Config),
        erlang:element(9, Config),
        erlang:element(10, Config),
        erlang:element(11, Config),
        erlang:element(12, Config),
        erlang:element(13, Config),
        erlang:element(14, Config),
        erlang:element(15, Config),
        erlang:element(16, Config),
        erlang:element(17, Config),
        erlang:element(18, Config),
        erlang:element(19, Config),
        erlang:element(20, Config),
        erlang:element(21, Config)}.

-file("src/spruce/box.gleam", 349).
-spec default_config(binary(), spruce@style:color()) -> box_config().
default_config(Title, Color) ->
    {box_config,
        Title,
        Color,
        0,
        1,
        0,
        1,
        0,
        0,
        0,
        0,
        rounded,
        none,
        Color,
        Color,
        Color,
        Color,
        true,
        true,
        true,
        true}.

-file("src/spruce/box.gleam", 299).
-spec box_config(box_options()) -> box_config().
box_config(Options) ->
    case Options of
        {box_options, Title, Color} ->
            default_config(Title, Color);

        {configured_box_options,
            Title@1,
            Color@1,
            Padding_top,
            Padding_right,
            Padding_bottom,
            Padding_left,
            Margin_top,
            Margin_right,
            Margin_bottom,
            Margin_left,
            Border,
            Width,
            Border_top_color,
            Border_right_color,
            Border_bottom_color,
            Border_left_color,
            Border_top,
            Border_right,
            Border_bottom,
            Border_left} ->
            {box_config,
                Title@1,
                Color@1,
                Padding_top,
                Padding_right,
                Padding_bottom,
                Padding_left,
                Margin_top,
                Margin_right,
                Margin_bottom,
                Margin_left,
                Border,
                Width,
                Border_top_color,
                Border_right_color,
                Border_bottom_color,
                Border_left_color,
                Border_top,
                Border_right,
                Border_bottom,
                Border_left}
    end.

-file("src/spruce/box.gleam", 113).
?DOC(" Set inner padding as top, right, bottom, left cell counts.\n").
-spec padding(box_options(), integer(), integer(), integer(), integer()) -> box_options().
padding(Options, Top, Right, Bottom, Left) ->
    Config = box_config(Options),
    configured(
        {box_config,
            erlang:element(2, Config),
            erlang:element(3, Config),
            non_negative(Top),
            non_negative(Right),
            non_negative(Bottom),
            non_negative(Left),
            erlang:element(8, Config),
            erlang:element(9, Config),
            erlang:element(10, Config),
            erlang:element(11, Config),
            erlang:element(12, Config),
            erlang:element(13, Config),
            erlang:element(14, Config),
            erlang:element(15, Config),
            erlang:element(16, Config),
            erlang:element(17, Config),
            erlang:element(18, Config),
            erlang:element(19, Config),
            erlang:element(20, Config),
            erlang:element(21, Config)}
    ).

-file("src/spruce/box.gleam", 134).
?DOC(" Set outer margin as top, right, bottom, left cell counts.\n").
-spec margin(box_options(), integer(), integer(), integer(), integer()) -> box_options().
margin(Options, Top, Right, Bottom, Left) ->
    Config = box_config(Options),
    configured(
        {box_config,
            erlang:element(2, Config),
            erlang:element(3, Config),
            erlang:element(4, Config),
            erlang:element(5, Config),
            erlang:element(6, Config),
            erlang:element(7, Config),
            non_negative(Top),
            non_negative(Right),
            non_negative(Bottom),
            non_negative(Left),
            erlang:element(12, Config),
            erlang:element(13, Config),
            erlang:element(14, Config),
            erlang:element(15, Config),
            erlang:element(16, Config),
            erlang:element(17, Config),
            erlang:element(18, Config),
            erlang:element(19, Config),
            erlang:element(20, Config),
            erlang:element(21, Config)}
    ).

-file("src/spruce/box.gleam", 155).
?DOC(" Set the border style.\n").
-spec border(box_options(), border()) -> box_options().
border(Options, Border) ->
    Config = box_config(Options),
    configured(
        {box_config,
            erlang:element(2, Config),
            erlang:element(3, Config),
            erlang:element(4, Config),
            erlang:element(5, Config),
            erlang:element(6, Config),
            erlang:element(7, Config),
            erlang:element(8, Config),
            erlang:element(9, Config),
            erlang:element(10, Config),
            erlang:element(11, Config),
            Border,
            erlang:element(13, Config),
            erlang:element(14, Config),
            erlang:element(15, Config),
            erlang:element(16, Config),
            erlang:element(17, Config),
            erlang:element(18, Config),
            erlang:element(19, Config),
            erlang:element(20, Config),
            erlang:element(21, Config)}
    ).

-file("src/spruce/box.gleam", 161).
?DOC(" Set top, right, bottom, and left border colors independently.\n").
-spec border_colors(
    box_options(),
    spruce@style:color(),
    spruce@style:color(),
    spruce@style:color(),
    spruce@style:color()
) -> box_options().
border_colors(Options, Top, Right, Bottom, Left) ->
    Config = box_config(Options),
    configured(
        {box_config,
            erlang:element(2, Config),
            erlang:element(3, Config),
            erlang:element(4, Config),
            erlang:element(5, Config),
            erlang:element(6, Config),
            erlang:element(7, Config),
            erlang:element(8, Config),
            erlang:element(9, Config),
            erlang:element(10, Config),
            erlang:element(11, Config),
            erlang:element(12, Config),
            erlang:element(13, Config),
            Top,
            Right,
            Bottom,
            Left,
            erlang:element(18, Config),
            erlang:element(19, Config),
            erlang:element(20, Config),
            erlang:element(21, Config)}
    ).

-file("src/spruce/box.gleam", 182).
?DOC(" Set top, right, bottom, and left border visibility independently.\n").
-spec border_sides(box_options(), boolean(), boolean(), boolean(), boolean()) -> box_options().
border_sides(Options, Top, Right, Bottom, Left) ->
    Config = box_config(Options),
    configured(
        {box_config,
            erlang:element(2, Config),
            erlang:element(3, Config),
            erlang:element(4, Config),
            erlang:element(5, Config),
            erlang:element(6, Config),
            erlang:element(7, Config),
            erlang:element(8, Config),
            erlang:element(9, Config),
            erlang:element(10, Config),
            erlang:element(11, Config),
            erlang:element(12, Config),
            erlang:element(13, Config),
            erlang:element(14, Config),
            erlang:element(15, Config),
            erlang:element(16, Config),
            erlang:element(17, Config),
            Top,
            Right,
            Bottom,
            Left}
    ).

-file("src/spruce/box.gleam", 203).
?DOC(" Constrain content to a visual width, wrapping with `spruce/align.wrap`.\n").
-spec width(box_options(), integer()) -> box_options().
width(Options, Width) ->
    Config = box_config(Options),
    configured(
        {box_config,
            erlang:element(2, Config),
            erlang:element(3, Config),
            erlang:element(4, Config),
            erlang:element(5, Config),
            erlang:element(6, Config),
            erlang:element(7, Config),
            erlang:element(8, Config),
            erlang:element(9, Config),
            erlang:element(10, Config),
            erlang:element(11, Config),
            erlang:element(12, Config),
            {some, non_negative(Width)},
            erlang:element(14, Config),
            erlang:element(15, Config),
            erlang:element(16, Config),
            erlang:element(17, Config),
            erlang:element(18, Config),
            erlang:element(19, Config),
            erlang:element(20, Config),
            erlang:element(21, Config)}
    ).

-file("src/spruce/box.gleam", 667).
-spec repeat_line_loop(binary(), integer(), list(binary())) -> list(binary()).
repeat_line_loop(Line, Count, Acc) ->
    case Count of
        0 ->
            Acc;

        _ ->
            repeat_line_loop(Line, Count - 1, [Line | Acc])
    end.

-file("src/spruce/box.gleam", 663).
-spec repeat_line(binary(), integer()) -> list(binary()).
repeat_line(Line, Count) ->
    repeat_line_loop(Line, non_negative(Count), []).

-file("src/spruce/box.gleam", 657).
-spec find_max_width(list(binary()), integer()) -> integer().
find_max_width(Lines, Min_width) ->
    _pipe = Lines,
    _pipe@1 = gleam@list:map(_pipe, fun spruce@align:visual_length/1),
    gleam@list:fold(_pipe@1, Min_width, fun gleam@int:max/2).

-file("src/spruce/box.gleam", 633).
-spec apply_margin(
    list(binary()),
    binary(),
    integer(),
    integer(),
    integer(),
    integer()
) -> list(binary()).
apply_margin(Lines, Prefix, Top, Right, Bottom, Left) ->
    Line_width = find_max_width(Lines, 0),
    Blank = gleam@string:repeat(<<" "/utf8>>, (Left + Line_width) + Right),
    Top_rows = repeat_line(<<Prefix/binary, Blank/binary>>, Top),
    Body = gleam@list:map(
        Lines,
        fun(Line) ->
            <<<<<<Prefix/binary,
                        (gleam@string:repeat(<<" "/utf8>>, Left))/binary>>/binary,
                    (spruce@align:pad_right(Line, Line_width))/binary>>/binary,
                (gleam@string:repeat(<<" "/utf8>>, Right))/binary>>
        end
    ),
    _pipe = Top_rows,
    _pipe@1 = lists:append(_pipe, Body),
    lists:append(_pipe@1, repeat_line(<<Prefix/binary, Blank/binary>>, Bottom)).

-file("src/spruce/box.gleam", 547).
-spec render_bottom(
    integer(),
    sides(),
    border_chars(),
    fun((binary()) -> binary())
) -> binary().
render_bottom(Inner_width, Sides, Chars, Paint_bottom) ->
    Left = case erlang:element(5, Sides) of
        true ->
            erlang:element(8, Chars);

        false ->
            <<""/utf8>>
    end,
    Right = case erlang:element(3, Sides) of
        true ->
            erlang:element(6, Chars);

        false ->
            <<""/utf8>>
    end,
    Paint_bottom(
        <<<<Left/binary,
                (gleam@string:repeat(erlang:element(7, Chars), Inner_width))/binary>>/binary,
            Right/binary>>
    ).

-file("src/spruce/box.gleam", 504).
-spec append_bottom(
    list(binary()),
    sides(),
    integer(),
    border_chars(),
    fun((binary()) -> binary())
) -> list(binary()).
append_bottom(Lines, Sides, Inner_width, Chars, Paint_bottom) ->
    gleam@bool:guard(
        erlang:element(4, Sides) =:= false,
        Lines,
        fun() ->
            lists:append(
                Lines,
                [render_bottom(Inner_width, Sides, Chars, Paint_bottom)]
            )
        end
    ).

-file("src/spruce/box.gleam", 516).
-spec render_top(
    binary(),
    integer(),
    integer(),
    sides(),
    border_chars(),
    fun((binary()) -> binary())
) -> binary().
render_top(Title, Title_width, Inner_width, Sides, Chars, Paint_top) ->
    Left = case erlang:element(5, Sides) of
        true ->
            erlang:element(2, Chars);

        false ->
            <<""/utf8>>
    end,
    Right = case erlang:element(3, Sides) of
        true ->
            erlang:element(4, Chars);

        false ->
            <<""/utf8>>
    end,
    case Title of
        <<""/utf8>> ->
            Paint_top(
                <<<<Left/binary,
                        (gleam@string:repeat(
                            erlang:element(3, Chars),
                            Inner_width
                        ))/binary>>/binary,
                    Right/binary>>
            );

        Title@1 ->
            <<<<<<<<(Paint_top(
                                <<Left/binary,
                                    (erlang:element(3, Chars))/binary>>
                            ))/binary,
                            " "/utf8>>/binary,
                        Title@1/binary>>/binary,
                    " "/utf8>>/binary,
                (Paint_top(
                    <<(gleam@string:repeat(
                            erlang:element(3, Chars),
                            gleam@int:max(0, (Inner_width - Title_width) - 3)
                        ))/binary,
                        Right/binary>>
                ))/binary>>
    end.

-file("src/spruce/box.gleam", 623).
-spec side_right(sides(), border_chars(), fun((binary()) -> binary())) -> binary().
side_right(Sides, Chars, Paint_right) ->
    gleam@bool:guard(
        erlang:element(3, Sides) =:= false,
        <<""/utf8>>,
        fun() -> Paint_right(erlang:element(5, Chars)) end
    ).

-file("src/spruce/box.gleam", 613).
-spec side_left(sides(), border_chars(), fun((binary()) -> binary())) -> binary().
side_left(Sides, Chars, Paint_left) ->
    gleam@bool:guard(
        erlang:element(5, Sides) =:= false,
        <<""/utf8>>,
        fun() -> Paint_left(erlang:element(9, Chars)) end
    ).

-file("src/spruce/box.gleam", 599).
-spec render_blank_row(
    integer(),
    sides(),
    border_chars(),
    fun((binary()) -> binary()),
    fun((binary()) -> binary())
) -> binary().
render_blank_row(Inner_width, Sides, Chars, Paint_left, Paint_right) ->
    Inner = gleam@string:repeat(<<" "/utf8>>, Inner_width),
    <<<<(side_left(Sides, Chars, Paint_left))/binary, Inner/binary>>/binary,
        (side_right(Sides, Chars, Paint_right))/binary>>.

-file("src/spruce/box.gleam", 585).
-spec blank_rows(
    integer(),
    integer(),
    sides(),
    border_chars(),
    fun((binary()) -> binary()),
    fun((binary()) -> binary())
) -> list(binary()).
blank_rows(Count, Inner_width, Sides, Chars, Paint_left, Paint_right) ->
    repeat_line(
        render_blank_row(Inner_width, Sides, Chars, Paint_left, Paint_right),
        Count
    ).

-file("src/spruce/box.gleam", 565).
-spec render_body_line(
    binary(),
    integer(),
    integer(),
    integer(),
    sides(),
    border_chars(),
    fun((binary()) -> binary()),
    fun((binary()) -> binary())
) -> binary().
render_body_line(
    Line,
    Text_width,
    Padding_left,
    Padding_right,
    Sides,
    Chars,
    Paint_left,
    Paint_right
) ->
    Inner = <<<<(gleam@string:repeat(<<" "/utf8>>, Padding_left))/binary,
            (spruce@align:pad_right(Line, Text_width))/binary>>/binary,
        (gleam@string:repeat(<<" "/utf8>>, Padding_right))/binary>>,
    <<<<(side_left(Sides, Chars, Paint_left))/binary, Inner/binary>>/binary,
        (side_right(Sides, Chars, Paint_right))/binary>>.

-file("src/spruce/box.gleam", 498).
-spec border_painter(spruce:spruce(), spruce@style:color()) -> fun((binary()) -> binary()).
border_painter(Sp, Color) ->
    Border_style = begin
        _pipe = spruce@style:new(),
        spruce@style:fg(_pipe, Color)
    end,
    fun(Text) -> spruce@style:render(Sp, Border_style, Text) end.

-file("src/spruce/box.gleam", 412).
-spec border_chars(border()) -> border_chars().
border_chars(Border) ->
    case Border of
        normal ->
            {border_chars,
                <<"┌"/utf8>>,
                <<"─"/utf8>>,
                <<"┐"/utf8>>,
                <<"│"/utf8>>,
                <<"┘"/utf8>>,
                <<"─"/utf8>>,
                <<"└"/utf8>>,
                <<"│"/utf8>>};

        rounded ->
            {border_chars,
                <<"╭"/utf8>>,
                <<"─"/utf8>>,
                <<"╮"/utf8>>,
                <<"│"/utf8>>,
                <<"╯"/utf8>>,
                <<"─"/utf8>>,
                <<"╰"/utf8>>,
                <<"│"/utf8>>};

        thick ->
            {border_chars,
                <<"┏"/utf8>>,
                <<"━"/utf8>>,
                <<"┓"/utf8>>,
                <<"┃"/utf8>>,
                <<"┛"/utf8>>,
                <<"━"/utf8>>,
                <<"┗"/utf8>>,
                <<"┃"/utf8>>};

        double ->
            {border_chars,
                <<"╔"/utf8>>,
                <<"═"/utf8>>,
                <<"╗"/utf8>>,
                <<"║"/utf8>>,
                <<"╝"/utf8>>,
                <<"═"/utf8>>,
                <<"╚"/utf8>>,
                <<"║"/utf8>>};

        block ->
            {border_chars,
                <<"█"/utf8>>,
                <<"█"/utf8>>,
                <<"█"/utf8>>,
                <<"█"/utf8>>,
                <<"█"/utf8>>,
                <<"█"/utf8>>,
                <<"█"/utf8>>,
                <<"█"/utf8>>};

        hidden ->
            {border_chars,
                <<""/utf8>>,
                <<""/utf8>>,
                <<""/utf8>>,
                <<""/utf8>>,
                <<""/utf8>>,
                <<""/utf8>>,
                <<""/utf8>>,
                <<""/utf8>>};

        {custom, Chars} ->
            Chars
    end.

-file("src/spruce/box.gleam", 491).
-spec sanitize_title(binary()) -> binary().
sanitize_title(Title) ->
    _pipe = Title,
    _pipe@1 = gleam@string:replace(_pipe, <<"\r\n"/utf8>>, <<" "/utf8>>),
    _pipe@2 = gleam@string:replace(_pipe@1, <<"\n"/utf8>>, <<" "/utf8>>),
    gleam@string:replace(_pipe@2, <<"\r"/utf8>>, <<" "/utf8>>).

-file("src/spruce/box.gleam", 484).
-spec wrap_content(binary(), gleam@option:option(integer())) -> binary().
wrap_content(Content, Width) ->
    case Width of
        {some, Width@1} when Width@1 > 0 ->
            spruce@align:wrap(Content, Width@1);

        _ ->
            Content
    end.

-file("src/spruce/box.gleam", 399).
-spec effective_sides(box_config()) -> sides().
effective_sides(Config) ->
    case erlang:element(12, Config) of
        hidden ->
            {sides, false, false, false, false};

        _ ->
            {sides,
                erlang:element(18, Config),
                erlang:element(19, Config),
                erlang:element(20, Config),
                erlang:element(21, Config)}
    end.

-file("src/spruce/box.gleam", 214).
?DOC(" Render `content` in a box with the given options.\n").
-spec render(spruce:spruce(), binary(), box_options()) -> binary().
render(Sp, Content, Options) ->
    Config = box_config(Options),
    Prefix = spruce@internal@layout:indent_prefix(Sp),
    Sides = effective_sides(Config),
    Lines = begin
        _pipe = Content,
        _pipe@1 = wrap_content(_pipe, erlang:element(13, Config)),
        gleam@string:split(_pipe@1, <<"\n"/utf8>>)
    end,
    Title = sanitize_title(erlang:element(2, Config)),
    Title_width = spruce@align:visual_length(Title),
    Title_min_width = case Title of
        <<""/utf8>> ->
            0;

        _ ->
            Title_width + 3
    end,
    Content_width = find_max_width(Lines, 0),
    Padded_content_width = (Content_width + erlang:element(7, Config)) + erlang:element(
        5,
        Config
    ),
    Inner_width = gleam@int:max(Padded_content_width, Title_min_width),
    Text_width = (Inner_width - erlang:element(7, Config)) - erlang:element(
        5,
        Config
    ),
    Chars = border_chars(erlang:element(12, Config)),
    Paint_top = border_painter(Sp, erlang:element(14, Config)),
    Paint_right = border_painter(Sp, erlang:element(15, Config)),
    Paint_bottom = border_painter(Sp, erlang:element(16, Config)),
    Paint_left = border_painter(Sp, erlang:element(17, Config)),
    Body = lists:append(
        blank_rows(
            erlang:element(4, Config),
            Inner_width,
            Sides,
            Chars,
            Paint_left,
            Paint_right
        ),
        begin
            Content_rows = gleam@list:map(
                Lines,
                fun(Line) ->
                    render_body_line(
                        Line,
                        Text_width,
                        erlang:element(7, Config),
                        erlang:element(5, Config),
                        Sides,
                        Chars,
                        Paint_left,
                        Paint_right
                    )
                end
            ),
            lists:append(
                Content_rows,
                blank_rows(
                    erlang:element(6, Config),
                    Inner_width,
                    Sides,
                    Chars,
                    Paint_left,
                    Paint_right
                )
            )
        end
    ),
    Boxed = begin
        _pipe@2 = case erlang:element(2, Sides) of
            true ->
                [render_top(
                        Title,
                        Title_width,
                        Inner_width,
                        Sides,
                        Chars,
                        Paint_top
                    ) |
                    Body];

            false ->
                Body
        end,
        append_bottom(_pipe@2, Sides, Inner_width, Chars, Paint_bottom)
    end,
    _pipe@3 = Boxed,
    _pipe@4 = apply_margin(
        _pipe@3,
        Prefix,
        erlang:element(8, Config),
        erlang:element(9, Config),
        erlang:element(10, Config),
        erlang:element(11, Config)
    ),
    gleam@string:join(_pipe@4, <<"\n"/utf8>>).

-file("src/spruce/box.gleam", 209).
?DOC(" Render `content` in a box using the default options.\n").
-spec simple(spruce:spruce(), binary()) -> binary().
simple(Sp, Content) ->
    render(Sp, Content, default_options()).

-file("src/spruce/box.gleam", 683).
?DOC(" Render a default box and print it to stdout.\n").
-spec print(spruce:spruce(), binary()) -> nil.
print(Sp, Content) ->
    gleam_stdlib:println(simple(Sp, Content)).