Skip to main content

src/spruce@align.erl

-module(spruce@align).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/spruce/align.gleam").
-export([visual_length/1, pad_right/2, pad_left/2, pad_center/2, height/1, size/1, truncate/3, wrap/2]).
-export_type([piece/0, piece_kind/0, escape_state/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(" ANSI-aware text alignment helpers.\n").

-type piece() :: {word, binary()} | {spaces, binary()}.

-type piece_kind() :: no_piece | word_piece | space_piece.

-type escape_state() :: normal_text |
    escape_start |
    csi_escape |
    osc_escape |
    osc_escape_after_esc.

-file("src/spruce/align.gleam", 149).
-spec in_range(integer(), integer(), integer()) -> boolean().
in_range(Value, First, Last) ->
    (Value >= First) andalso (Value =< Last).

-file("src/spruce/align.gleam", 76).
-spec is_escape_final(binary()) -> boolean().
is_escape_final(Grapheme) ->
    case gleam@string:to_utf_codepoints(Grapheme) of
        [] ->
            false;

        [Codepoint | _] ->
            in_range(gleam_stdlib:identity(Codepoint), 16#40, 16#7e)
    end.

-file("src/spruce/align.gleam", 84).
-spec step_escape_state(escape_state(), binary()) -> escape_state().
step_escape_state(State, Char) ->
    case State of
        normal_text ->
            case Char of
                <<"\x{001b}"/utf8>> ->
                    escape_start;

                _ ->
                    normal_text
            end;

        escape_start ->
            case Char of
                <<"["/utf8>> ->
                    csi_escape;

                <<"]"/utf8>> ->
                    osc_escape;

                <<"\x{001b}"/utf8>> ->
                    escape_start;

                _ ->
                    normal_text
            end;

        csi_escape ->
            case is_escape_final(Char) of
                true ->
                    normal_text;

                false ->
                    csi_escape
            end;

        osc_escape ->
            case Char of
                <<"\x{0007}"/utf8>> ->
                    normal_text;

                <<"\x{001b}"/utf8>> ->
                    osc_escape_after_esc;

                _ ->
                    osc_escape
            end;

        osc_escape_after_esc ->
            case Char of
                <<"\\"/utf8>> ->
                    normal_text;

                <<"\x{0007}"/utf8>> ->
                    normal_text;

                <<"\x{001b}"/utf8>> ->
                    osc_escape_after_esc;

                _ ->
                    osc_escape
            end
    end.

-file("src/spruce/align.gleam", 132).
-spec is_wide(integer()) -> boolean().
is_wide(Codepoint) ->
    ((((((((((((in_range(Codepoint, 16#1100, 16#115f) orelse in_range(
        Codepoint,
        16#2e80,
        16#303e
    ))
    orelse in_range(Codepoint, 16#3041, 16#33ff))
    orelse in_range(Codepoint, 16#3400, 16#4dbf))
    orelse in_range(Codepoint, 16#4e00, 16#9fff))
    orelse in_range(Codepoint, 16#a000, 16#a4cf))
    orelse in_range(Codepoint, 16#ac00, 16#d7a3))
    orelse in_range(Codepoint, 16#f900, 16#faff))
    orelse in_range(Codepoint, 16#fe10, 16#fe19))
    orelse in_range(Codepoint, 16#fe30, 16#fe6f))
    orelse in_range(Codepoint, 16#ff00, 16#ff60))
    orelse in_range(Codepoint, 16#ffe0, 16#ffe6))
    orelse in_range(Codepoint, 16#1f300, 16#1faff))
    orelse in_range(Codepoint, 16#20000, 16#3fffd).

-file("src/spruce/align.gleam", 123).
-spec is_zero_width(integer()) -> boolean().
is_zero_width(Codepoint) ->
    ((((in_range(Codepoint, 16#0300, 16#036f) orelse (Codepoint =:= 16#0489))
    orelse in_range(Codepoint, 16#200b, 16#200f))
    orelse (Codepoint =:= 16#200d))
    orelse in_range(Codepoint, 16#fe00, 16#fe0f))
    orelse (Codepoint =:= 16#feff).

-file("src/spruce/align.gleam", 51).
-spec display_width(binary()) -> integer().
display_width(Grapheme) ->
    case gleam@string:to_utf_codepoints(Grapheme) of
        [] ->
            0;

        [Codepoint | _] ->
            Codepoint@1 = gleam_stdlib:identity(Codepoint),
            case is_zero_width(Codepoint@1) of
                true ->
                    0;

                false ->
                    case is_wide(Codepoint@1) of
                        true ->
                            2;

                        false ->
                            1
                    end
            end
    end.

-file("src/spruce/align.gleam", 35).
-spec count_visible(list(binary()), escape_state(), integer()) -> integer().
count_visible(Chars, State, Count) ->
    case Chars of
        [] ->
            Count;

        [Char | Rest] ->
            case State of
                normal_text ->
                    case Char of
                        <<"\x{001b}"/utf8>> ->
                            count_visible(
                                Rest,
                                step_escape_state(normal_text, Char),
                                Count
                            );

                        _ ->
                            count_visible(
                                Rest,
                                normal_text,
                                Count + display_width(Char)
                            )
                    end;

                _ ->
                    count_visible(Rest, step_escape_state(State, Char), Count)
            end
    end.

-file("src/spruce/align.gleam", 69).
-spec is_ascii(binary()) -> boolean().
is_ascii(Text) ->
    _pipe = gleam@string:to_utf_codepoints(Text),
    gleam@list:fold(
        _pipe,
        true,
        fun(All_ascii, Codepoint) ->
            All_ascii andalso (gleam_stdlib:identity(Codepoint) < 16#80)
        end
    ).

-file("src/spruce/align.gleam", 28).
?DOC(" The visual length of a string, excluding ANSI escape codes.\n").
-spec visual_length(binary()) -> integer().
visual_length(Text) ->
    case gleam_stdlib:contains_string(Text, <<"\x{001b}"/utf8>>) orelse not is_ascii(
        Text
    ) of
        true ->
            count_visible(gleam@string:to_graphemes(Text), normal_text, 0);

        false ->
            string:length(Text)
    end.

-file("src/spruce/align.gleam", 155).
?DOC(
    " Pad `text` on the right with spaces until it reaches `width` visual columns.\n"
    " Text already at or beyond `width` is returned unchanged.\n"
).
-spec pad_right(binary(), integer()) -> binary().
pad_right(Text, Width) ->
    Padding = Width - visual_length(Text),
    case Padding > 0 of
        true ->
            <<Text/binary, (gleam@string:repeat(<<" "/utf8>>, Padding))/binary>>;

        false ->
            Text
    end.

-file("src/spruce/align.gleam", 165).
?DOC(
    " Pad `text` on the left with spaces until it reaches `width` visual columns.\n"
    " Text already at or beyond `width` is returned unchanged.\n"
).
-spec pad_left(binary(), integer()) -> binary().
pad_left(Text, Width) ->
    Padding = Width - visual_length(Text),
    case Padding > 0 of
        true ->
            <<(gleam@string:repeat(<<" "/utf8>>, Padding))/binary, Text/binary>>;

        false ->
            Text
    end.

-file("src/spruce/align.gleam", 176).
?DOC(
    " Pad `text` on both sides with spaces until it reaches `width` visual columns.\n"
    " When an odd number of spaces is needed, the extra space is placed on the\n"
    " right. Text already at or beyond `width` is returned unchanged.\n"
).
-spec pad_center(binary(), integer()) -> binary().
pad_center(Text, Width) ->
    Padding = Width - visual_length(Text),
    case Padding > 0 of
        true ->
            Left = Padding div 2,
            Right = Padding - Left,
            <<<<(gleam@string:repeat(<<" "/utf8>>, Left))/binary, Text/binary>>/binary,
                (gleam@string:repeat(<<" "/utf8>>, Right))/binary>>;

        false ->
            Text
    end.

-file("src/spruce/align.gleam", 189).
?DOC(" Count the number of lines in `text`.\n").
-spec height(binary()) -> integer().
height(Text) ->
    _pipe = Text,
    _pipe@1 = gleam@string:split(_pipe, <<"\n"/utf8>>),
    erlang:length(_pipe@1).

-file("src/spruce/align.gleam", 196).
?DOC(" Return the widest visual line and the number of lines in `text`.\n").
-spec size(binary()) -> {integer(), integer()}.
size(Text) ->
    Lines = gleam@string:split(Text, <<"\n"/utf8>>),
    Width = begin
        _pipe = Lines,
        _pipe@1 = gleam@list:map(_pipe, fun visual_length/1),
        gleam@list:fold(_pipe@1, 0, fun gleam@int:max/2)
    end,
    {Width, erlang:length(Lines)}.

-file("src/spruce/align.gleam", 707).
-spec chars_to_string(list(binary())) -> binary().
chars_to_string(Chars) ->
    gleam@string:join(Chars, <<""/utf8>>).

-file("src/spruce/align.gleam", 615).
-spec take_visible_loop(
    list(binary()),
    integer(),
    escape_state(),
    list(binary())
) -> binary().
take_visible_loop(Chars, Remaining, State, Acc) ->
    case Chars of
        [] ->
            chars_to_string(lists:reverse(Acc));

        [Char | Rest] ->
            case State of
                normal_text ->
                    case Char of
                        <<"\x{001b}"/utf8>> ->
                            take_visible_loop(
                                Rest,
                                Remaining,
                                step_escape_state(normal_text, Char),
                                [Char | Acc]
                            );

                        _ ->
                            Char_width = display_width(Char),
                            case (Char_width =:= 0) orelse (Char_width =< Remaining) of
                                true ->
                                    take_visible_loop(
                                        Rest,
                                        Remaining - Char_width,
                                        normal_text,
                                        [Char | Acc]
                                    );

                                false ->
                                    take_visible_loop(Rest, 0, normal_text, Acc)
                            end
                    end;

                _ ->
                    take_visible_loop(
                        Rest,
                        Remaining,
                        step_escape_state(State, Char),
                        [Char | Acc]
                    )
            end
    end.

-file("src/spruce/align.gleam", 608).
-spec take_visible(binary(), integer()) -> binary().
take_visible(Text, Width) ->
    case Width =< 0 of
        true ->
            take_visible_loop(
                gleam@string:to_graphemes(Text),
                0,
                normal_text,
                []
            );

        false ->
            take_visible_loop(
                gleam@string:to_graphemes(Text),
                Width,
                normal_text,
                []
            )
    end.

-file("src/spruce/align.gleam", 544).
-spec sgr_has_param(binary(), binary()) -> boolean().
sgr_has_param(Escape, Param) ->
    ((gleam_stdlib:contains_string(
        Escape,
        <<<<"["/utf8, Param/binary>>/binary, "m"/utf8>>
    )
    orelse gleam_stdlib:contains_string(
        Escape,
        <<<<"["/utf8, Param/binary>>/binary, ";"/utf8>>
    ))
    orelse gleam_stdlib:contains_string(
        Escape,
        <<<<";"/utf8, Param/binary>>/binary, "m"/utf8>>
    ))
    orelse gleam_stdlib:contains_string(
        Escape,
        <<<<";"/utf8, Param/binary>>/binary, ";"/utf8>>
    ).

-file("src/spruce/align.gleam", 537).
-spec is_sgr_reset(binary()) -> boolean().
is_sgr_reset(Escape) ->
    ((gleam_stdlib:contains_string(Escape, <<"[m"/utf8>>) orelse sgr_has_param(
        Escape,
        <<"0"/utf8>>
    ))
    orelse sgr_has_param(Escape, <<"39"/utf8>>))
    orelse sgr_has_param(Escape, <<"49"/utf8>>).

-file("src/spruce/align.gleam", 530).
-spec update_sgr_active(binary(), binary(), boolean()) -> boolean().
update_sgr_active(Escape, Final, Active) ->
    case Final of
        <<"m"/utf8>> ->
            not is_sgr_reset(Escape);

        _ ->
            Active
    end.

-file("src/spruce/align.gleam", 473).
-spec has_open_sgr_loop(list(binary()), escape_state(), binary(), boolean()) -> boolean().
has_open_sgr_loop(Chars, State, Escape, Active) ->
    case Chars of
        [] ->
            Active;

        [Char | Rest] ->
            case State of
                normal_text ->
                    case Char of
                        <<"\x{001b}"/utf8>> ->
                            has_open_sgr_loop(
                                Rest,
                                step_escape_state(normal_text, Char),
                                Char,
                                Active
                            );

                        _ ->
                            has_open_sgr_loop(
                                Rest,
                                normal_text,
                                <<""/utf8>>,
                                Active
                            )
                    end;

                escape_start ->
                    case Char of
                        <<"["/utf8>> ->
                            has_open_sgr_loop(
                                Rest,
                                csi_escape,
                                <<Escape/binary, Char/binary>>,
                                Active
                            );

                        <<"]"/utf8>> ->
                            has_open_sgr_loop(
                                Rest,
                                osc_escape,
                                <<""/utf8>>,
                                Active
                            );

                        _ ->
                            has_open_sgr_loop(
                                Rest,
                                step_escape_state(escape_start, Char),
                                <<""/utf8>>,
                                Active
                            )
                    end;

                csi_escape ->
                    Escape@1 = <<Escape/binary, Char/binary>>,
                    Active@1 = case is_escape_final(Char) of
                        true ->
                            update_sgr_active(Escape@1, Char, Active);

                        false ->
                            Active
                    end,
                    has_open_sgr_loop(
                        Rest,
                        step_escape_state(csi_escape, Char),
                        Escape@1,
                        Active@1
                    );

                osc_escape ->
                    has_open_sgr_loop(
                        Rest,
                        step_escape_state(State, Char),
                        <<""/utf8>>,
                        Active
                    );

                osc_escape_after_esc ->
                    has_open_sgr_loop(
                        Rest,
                        step_escape_state(State, Char),
                        <<""/utf8>>,
                        Active
                    )
            end
    end.

-file("src/spruce/align.gleam", 469).
-spec has_open_sgr(binary()) -> boolean().
has_open_sgr(Text) ->
    has_open_sgr_loop(
        gleam@string:to_graphemes(Text),
        normal_text,
        <<""/utf8>>,
        false
    ).

-file("src/spruce/align.gleam", 464).
-spec close_open_sgr(binary()) -> binary().
close_open_sgr(Text) ->
    gleam@bool:guard(
        not has_open_sgr(Text),
        Text,
        fun() -> <<Text/binary, "\x{001b}[0m"/utf8>> end
    ).

-file("src/spruce/align.gleam", 212).
?DOC(
    " Truncate `text` to at most `width` visual columns, appending `ellipsis` when\n"
    " truncation is needed.\n"
    "\n"
    " ANSI escape sequences do not count toward the width and are never split.\n"
    " If `ellipsis` is wider than `width`, the ellipsis itself is visibly\n"
    " truncated to fit. Widths less than or equal to zero return an empty string.\n"
).
-spec truncate(binary(), integer(), binary()) -> binary().
truncate(Text, Width, Ellipsis) ->
    gleam@bool:guard(
        Width =< 0,
        <<""/utf8>>,
        fun() ->
            gleam@bool:guard(
                visual_length(Text) =< Width,
                Text,
                fun() ->
                    Ellipsis_width = visual_length(Ellipsis),
                    case Ellipsis_width >= Width of
                        true ->
                            take_visible(Ellipsis, Width);

                        false ->
                            <<(close_open_sgr(
                                    take_visible(Text, Width - Ellipsis_width)
                                ))/binary,
                                Ellipsis/binary>>
                    end
                end
            )
        end
    ).

-file("src/spruce/align.gleam", 354).
-spec drop_spaces(list(binary())) -> list(binary()).
drop_spaces(Chars) ->
    case Chars of
        [<<" "/utf8>> | Rest] ->
            drop_spaces(Rest);

        _ ->
            Chars
    end.

-file("src/spruce/align.gleam", 345).
-spec trim_trailing_spaces(binary()) -> binary().
trim_trailing_spaces(Text) ->
    _pipe = Text,
    _pipe@1 = gleam@string:to_graphemes(_pipe),
    _pipe@2 = lists:reverse(_pipe@1),
    _pipe@3 = drop_spaces(_pipe@2),
    _pipe@4 = lists:reverse(_pipe@3),
    chars_to_string(_pipe@4).

-file("src/spruce/align.gleam", 338).
-spec push_wrapped_line(binary(), list(binary())) -> list(binary()).
push_wrapped_line(Line, Lines) ->
    case trim_trailing_spaces(Line) of
        <<""/utf8>> ->
            Lines;

        Trimmed ->
            [close_open_sgr(Trimmed) | Lines]
    end.

-file("src/spruce/align.gleam", 663).
-spec drop_visible_loop(list(binary()), integer(), escape_state()) -> binary().
drop_visible_loop(Chars, Remaining, State) ->
    case Chars of
        [] ->
            <<""/utf8>>;

        [Char | Rest] ->
            case State of
                normal_text ->
                    case Remaining =< 0 of
                        true ->
                            chars_to_string(Chars);

                        false ->
                            case Char of
                                <<"\x{001b}"/utf8>> ->
                                    drop_visible_loop(
                                        Rest,
                                        Remaining,
                                        step_escape_state(normal_text, Char)
                                    );

                                _ ->
                                    Char_width = display_width(Char),
                                    case Char_width > Remaining of
                                        true ->
                                            chars_to_string(Chars);

                                        false ->
                                            drop_visible_loop(
                                                Rest,
                                                Remaining - Char_width,
                                                normal_text
                                            )
                                    end
                            end
                    end;

                _ ->
                    drop_visible_loop(
                        Rest,
                        Remaining,
                        step_escape_state(State, Char)
                    )
            end
    end.

-file("src/spruce/align.gleam", 659).
-spec drop_visible(binary(), integer()) -> binary().
drop_visible(Text, Width) ->
    drop_visible_loop(gleam@string:to_graphemes(Text), Width, normal_text).

-file("src/spruce/align.gleam", 580).
-spec first_positive_width_loop(list(binary()), escape_state()) -> integer().
first_positive_width_loop(Chars, State) ->
    case Chars of
        [] ->
            0;

        [Char | Rest] ->
            case State of
                normal_text ->
                    case Char of
                        <<"\x{001b}"/utf8>> ->
                            first_positive_width_loop(
                                Rest,
                                step_escape_state(normal_text, Char)
                            );

                        _ ->
                            Width = display_width(Char),
                            case Width of
                                0 ->
                                    first_positive_width_loop(Rest, normal_text);

                                _ ->
                                    Width
                            end
                    end;

                _ ->
                    first_positive_width_loop(
                        Rest,
                        step_escape_state(State, Char)
                    )
            end
    end.

-file("src/spruce/align.gleam", 576).
-spec first_positive_width(binary()) -> integer().
first_positive_width(Text) ->
    first_positive_width_loop(gleam@string:to_graphemes(Text), normal_text).

-file("src/spruce/align.gleam", 551).
-spec wrap_long_word(binary(), integer()) -> list(binary()).
wrap_long_word(Word, Width) ->
    case visual_length(Word) =< Width of
        true ->
            [Word];

        false ->
            Chunk = take_visible(Word, Width),
            case visual_length(Chunk) =:= 0 of
                true ->
                    First_width = first_positive_width(Word),
                    case First_width of
                        0 ->
                            [Word];

                        _ ->
                            [take_visible(Word, First_width) |
                                wrap_long_word(
                                    drop_visible(Word, First_width),
                                    Width
                                )]
                    end;

                false ->
                    [Chunk | wrap_long_word(drop_visible(Word, Width), Width)]
            end
    end.

-file("src/spruce/align.gleam", 452).
-spec add_wrapped_chunks(list(binary()), list(binary())) -> {binary(),
    integer(),
    list(binary())}.
add_wrapped_chunks(Chunks, Lines) ->
    case Chunks of
        [] ->
            {<<""/utf8>>, 0, Lines};

        [Chunk] ->
            {Chunk, visual_length(Chunk), Lines};

        [Chunk@1 | Rest] ->
            add_wrapped_chunks(Rest, [close_open_sgr(Chunk@1) | Lines])
    end.

-file("src/spruce/align.gleam", 248).
-spec wrap_pieces(list(piece()), integer(), binary(), integer(), list(binary())) -> list(binary()).
wrap_pieces(Pieces, Width, Current, Current_width, Lines) ->
    case Pieces of
        [] ->
            case Current of
                <<""/utf8>> ->
                    lists:reverse(Lines);

                _ ->
                    lists:reverse([Current | Lines])
            end;

        [{spaces, Spaces} | Rest] ->
            Spaces_width = visual_length(Spaces),
            case (Current_width + Spaces_width) =< Width of
                true ->
                    wrap_pieces(
                        Rest,
                        Width,
                        <<Current/binary, Spaces/binary>>,
                        Current_width + Spaces_width,
                        Lines
                    );

                false ->
                    case Current_width of
                        0 ->
                            {Next_current, Next_width, Next_lines} = add_wrapped_chunks(
                                wrap_long_word(Spaces, Width),
                                Lines
                            ),
                            wrap_pieces(
                                Rest,
                                Width,
                                Next_current,
                                Next_width,
                                Next_lines
                            );

                        _ ->
                            wrap_pieces(
                                Rest,
                                Width,
                                <<""/utf8>>,
                                0,
                                push_wrapped_line(Current, Lines)
                            )
                    end
            end;

        [{word, Word} | Rest@1] ->
            Word_width = visual_length(Word),
            case Word_width > Width of
                true ->
                    case Current_width of
                        0 ->
                            {Next_current@1, Next_width@1, Next_lines@1} = add_wrapped_chunks(
                                wrap_long_word(Word, Width),
                                Lines
                            ),
                            wrap_pieces(
                                Rest@1,
                                Width,
                                Next_current@1,
                                Next_width@1,
                                Next_lines@1
                            );

                        _ ->
                            wrap_pieces(
                                Pieces,
                                Width,
                                <<""/utf8>>,
                                0,
                                push_wrapped_line(Current, Lines)
                            )
                    end;

                false ->
                    case (Current_width + Word_width) =< Width of
                        true ->
                            wrap_pieces(
                                Rest@1,
                                Width,
                                <<Current/binary, Word/binary>>,
                                Current_width + Word_width,
                                Lines
                            );

                        false ->
                            wrap_pieces(
                                Pieces,
                                Width,
                                <<""/utf8>>,
                                0,
                                push_wrapped_line(Current, Lines)
                            )
                    end
            end
    end.

-file("src/spruce/align.gleam", 437).
-spec push_piece(piece_kind(), binary(), list(piece())) -> list(piece()).
push_piece(Kind, Current, Pieces) ->
    case Current of
        <<""/utf8>> ->
            Pieces;

        _ ->
            case Kind of
                space_piece ->
                    [{spaces, Current} | Pieces];

                _ ->
                    [{word, Current} | Pieces]
            end
    end.

-file("src/spruce/align.gleam", 422).
-spec add_piece_grapheme(
    binary(),
    piece_kind(),
    binary(),
    piece_kind(),
    list(piece())
) -> {binary(), piece_kind(), list(piece())}.
add_piece_grapheme(Char, Char_kind, Current, Kind, Pieces) ->
    case {Kind, Char_kind} of
        {no_piece, _} ->
            {Char, Char_kind, Pieces};

        {space_piece, space_piece} ->
            {<<Current/binary, Char/binary>>, Kind, Pieces};

        {word_piece, word_piece} ->
            {<<Current/binary, Char/binary>>, Kind, Pieces};

        {_, _} ->
            {Char, Char_kind, push_piece(Kind, Current, Pieces)}
    end.

-file("src/spruce/align.gleam", 365).
-spec split_pieces_loop(
    list(binary()),
    escape_state(),
    binary(),
    piece_kind(),
    list(piece())
) -> list(piece()).
split_pieces_loop(Chars, State, Current, Kind, Pieces) ->
    case Chars of
        [] ->
            lists:reverse(push_piece(Kind, Current, Pieces));

        [Char | Rest] ->
            case State of
                normal_text ->
                    case Char of
                        <<"\x{001b}"/utf8>> ->
                            Kind@1 = case Kind of
                                no_piece ->
                                    word_piece;

                                _ ->
                                    Kind
                            end,
                            split_pieces_loop(
                                Rest,
                                step_escape_state(normal_text, Char),
                                <<Current/binary, Char/binary>>,
                                Kind@1,
                                Pieces
                            );

                        <<" "/utf8>> ->
                            {Current@1, Kind@2, Pieces@1} = add_piece_grapheme(
                                <<" "/utf8>>,
                                space_piece,
                                Current,
                                Kind,
                                Pieces
                            ),
                            split_pieces_loop(
                                Rest,
                                normal_text,
                                Current@1,
                                Kind@2,
                                Pieces@1
                            );

                        _ ->
                            {Current@2, Kind@3, Pieces@2} = add_piece_grapheme(
                                Char,
                                word_piece,
                                Current,
                                Kind,
                                Pieces
                            ),
                            split_pieces_loop(
                                Rest,
                                normal_text,
                                Current@2,
                                Kind@3,
                                Pieces@2
                            )
                    end;

                _ ->
                    split_pieces_loop(
                        Rest,
                        step_escape_state(State, Char),
                        <<Current/binary, Char/binary>>,
                        Kind,
                        Pieces
                    )
            end
    end.

-file("src/spruce/align.gleam", 361).
-spec split_pieces(binary()) -> list(piece()).
split_pieces(Text) ->
    split_pieces_loop(
        gleam@string:to_graphemes(Text),
        normal_text,
        <<""/utf8>>,
        no_piece,
        []
    ).

-file("src/spruce/align.gleam", 241).
-spec wrap_line(binary(), integer()) -> binary().
wrap_line(Line, Width) ->
    _pipe = Line,
    _pipe@1 = split_pieces(_pipe),
    _pipe@2 = wrap_pieces(_pipe@1, Width, <<""/utf8>>, 0, []),
    gleam@string:join(_pipe@2, <<"\n"/utf8>>).

-file("src/spruce/align.gleam", 233).
?DOC(
    " Wrap `text` to `width` visual columns.\n"
    "\n"
    " ANSI escape sequences do not count toward the width and are never split.\n"
    " Words longer than `width` are hard-wrapped at visible column boundaries.\n"
    " Widths less than or equal to zero return the input unchanged.\n"
).
-spec wrap(binary(), integer()) -> binary().
wrap(Text, Width) ->
    gleam@bool:guard(Width =< 0, Text, fun() -> _pipe = Text,
            _pipe@1 = gleam@string:split(_pipe, <<"\n"/utf8>>),
            _pipe@2 = gleam@list:map(
                _pipe@1,
                fun(_capture) -> wrap_line(_capture, Width) end
            ),
            gleam@string:join(_pipe@2, <<"\n"/utf8>>) end).