Skip to main content

src/etui@buffer.erl

-module(etui@buffer).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/etui/buffer.gleam").
-export([continuation_cell/3, area/1, width/1, height/1, cell_symbol/1, cell_fg/1, cell_bg/1, cell_modifier/1, is_continuation/1, empty_cell/0, cell_link/1, buffer_new/1, buffer_new_filled/5, get_cell/2, set_cell/3, set_string_linked/7, set_string/6, clear/2, diff/2, to_ansi/1, patches_to_ansi/1, diff_to_ansi/2]).
-export_type([cell_array/0, cell_content/0, cell/0, buffer/0, buffer_op/0, buf_view/0, run_style/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.

-type cell_array() :: any().

-type cell_content() :: {content, binary(), integer()} | continuation.

-type cell() :: {cell,
        cell_content(),
        etui@style:color(),
        etui@style:color(),
        etui@style:modifier(),
        binary()}.

-opaque buffer() :: {buffer, etui@geometry:rect(), cell_array()}.

-type buffer_op() :: {patch, etui@geometry:position(), list(cell())}.

-type buf_view() :: {buf_view,
        cell_array(),
        integer(),
        integer(),
        integer(),
        integer(),
        integer()}.

-type run_style() :: {run_style,
        etui@style:color(),
        etui@style:color(),
        etui@style:modifier(),
        binary()}.

-file("src/etui/buffer.gleam", 282).
?DOC(" Continuation cell (second column of a wide grapheme).\n").
-spec continuation_cell(
    etui@style:color(),
    etui@style:color(),
    etui@style:modifier()
) -> cell().
continuation_cell(Fg, Bg, Modifier) ->
    {cell, continuation, Fg, Bg, Modifier, <<""/utf8>>}.

-file("src/etui/buffer.gleam", 125).
-spec fill_graphemes(
    cell_array(),
    integer(),
    integer(),
    list(binary()),
    etui@style:color(),
    etui@style:color(),
    etui@style:modifier(),
    binary()
) -> cell_array().
fill_graphemes(Arr, Idx, Max_idx, Gs, Fg, Bg, Modifier, Link) ->
    case Idx >= Max_idx of
        true ->
            Arr;

        false ->
            case Gs of
                [] ->
                    Arr;

                [G | Rest] ->
                    W = etui@text:grapheme_cell_width(G),
                    Cell = {cell, {content, G, W}, Fg, Bg, Modifier, Link},
                    Arr2 = etui_buffer_array_ffi:set(Idx, Cell, Arr),
                    case W >= 2 of
                        true ->
                            Arr3 = case (Idx + 1) < Max_idx of
                                true ->
                                    etui_buffer_array_ffi:set(
                                        Idx + 1,
                                        continuation_cell(Fg, Bg, Modifier),
                                        Arr2
                                    );

                                false ->
                                    Arr2
                            end,
                            fill_graphemes(
                                Arr3,
                                Idx + 2,
                                Max_idx,
                                Rest,
                                Fg,
                                Bg,
                                Modifier,
                                Link
                            );

                        false ->
                            fill_graphemes(
                                Arr2,
                                Idx + 1,
                                Max_idx,
                                Rest,
                                Fg,
                                Bg,
                                Modifier,
                                Link
                            )
                    end
            end
    end.

-file("src/etui/buffer.gleam", 58).
-spec fill_all_rows_gleam(
    cell_array(),
    integer(),
    integer(),
    integer(),
    binary(),
    etui@style:color(),
    etui@style:color(),
    etui@style:modifier(),
    binary()
) -> cell_array().
fill_all_rows_gleam(Arr, Row, Height, Width, Str, Fg, Bg, Modifier, Link) ->
    case Row >= Height of
        true ->
            Arr;

        false ->
            Start = Row * Width,
            Arr2 = fill_graphemes(
                Arr,
                Start,
                Start + Width,
                gleam@string:to_graphemes(Str),
                Fg,
                Bg,
                Modifier,
                Link
            ),
            fill_all_rows_gleam(
                Arr2,
                Row + 1,
                Height,
                Width,
                Str,
                Fg,
                Bg,
                Modifier,
                Link
            )
    end.

-file("src/etui/buffer.gleam", 222).
?DOC(" The rect this buffer covers.\n").
-spec area(buffer()) -> etui@geometry:rect().
area(Buf) ->
    erlang:element(2, Buf).

-file("src/etui/buffer.gleam", 227).
?DOC(" Width in cells.\n").
-spec width(buffer()) -> integer().
width(Buf) ->
    erlang:element(2, erlang:element(3, erlang:element(2, Buf))).

-file("src/etui/buffer.gleam", 232).
?DOC(" Height in rows.\n").
-spec height(buffer()) -> integer().
height(Buf) ->
    erlang:element(3, erlang:element(3, erlang:element(2, Buf))).

-file("src/etui/buffer.gleam", 237).
?DOC(" Symbol string of a cell. Returns \" \" for Continuation cells.\n").
-spec cell_symbol(cell()) -> binary().
cell_symbol(Cell) ->
    case erlang:element(2, Cell) of
        {content, S, _} ->
            S;

        continuation ->
            <<" "/utf8>>
    end.

-file("src/etui/buffer.gleam", 245).
?DOC(" Foreground color of a cell.\n").
-spec cell_fg(cell()) -> etui@style:color().
cell_fg(Cell) ->
    erlang:element(3, Cell).

-file("src/etui/buffer.gleam", 250).
?DOC(" Background color of a cell.\n").
-spec cell_bg(cell()) -> etui@style:color().
cell_bg(Cell) ->
    erlang:element(4, Cell).

-file("src/etui/buffer.gleam", 255).
?DOC(" Text modifier of a cell.\n").
-spec cell_modifier(cell()) -> etui@style:modifier().
cell_modifier(Cell) ->
    erlang:element(5, Cell).

-file("src/etui/buffer.gleam", 260).
?DOC(" True if this cell is the second column of a wide grapheme (never rendered directly).\n").
-spec is_continuation(cell()) -> boolean().
is_continuation(Cell) ->
    case erlang:element(2, Cell) of
        continuation ->
            true;

        _ ->
            false
    end.

-file("src/etui/buffer.gleam", 271).
?DOC(" Empty cell (space, default style, no link).\n").
-spec empty_cell() -> cell().
empty_cell() ->
    {cell,
        {content, <<" "/utf8>>, 1},
        default,
        default,
        etui@style:none(),
        <<""/utf8>>}.

-file("src/etui/buffer.gleam", 291).
?DOC(" Accessor: OSC 8 hyperlink URI of a cell (empty = no link).\n").
-spec cell_link(cell()) -> binary().
cell_link(Cell) ->
    erlang:element(6, Cell).

-file("src/etui/buffer.gleam", 296).
?DOC(" New buffer with given area. All cells start as `empty_cell()`.\n").
-spec buffer_new(etui@geometry:rect()) -> buffer().
buffer_new(Area) ->
    Size = gleam@int:max(
        erlang:element(2, erlang:element(3, Area)) * erlang:element(
            3,
            erlang:element(3, Area)
        ),
        0
    ),
    {buffer, Area, etui_buffer_array_ffi:new(Size, empty_cell())}.

-file("src/etui/buffer.gleam", 304).
?DOC(
    " Create a buffer with every row pre-filled with `row_text`.\n"
    " Uses bulk array construction: one pass instead of `buffer_new` followed by\n"
    " a `set_string` for every row.\n"
).
-spec buffer_new_filled(
    etui@geometry:rect(),
    binary(),
    etui@style:color(),
    etui@style:color(),
    etui@style:modifier()
) -> buffer().
buffer_new_filled(Area, Row_text, Fg, Bg, Modifier) ->
    Default = empty_cell(),
    {buffer,
        Area,
        etui_buffer_array_ffi:fill_all_rows(
            erlang:element(2, erlang:element(3, Area)),
            erlang:element(3, erlang:element(3, Area)),
            Row_text,
            Fg,
            Bg,
            Modifier,
            <<""/utf8>>,
            Default
        )}.

-file("src/etui/buffer.gleam", 330).
-spec pos_to_idx(etui@geometry:rect(), etui@geometry:position()) -> integer().
pos_to_idx(Area, Pos) ->
    ((erlang:element(3, Pos) - erlang:element(3, erlang:element(2, Area))) * erlang:element(
        2,
        erlang:element(3, Area)
    ))
    + (erlang:element(2, Pos) - erlang:element(2, erlang:element(2, Area))).

-file("src/etui/buffer.gleam", 338).
?DOC(" Get cell at position. Returns empty_cell() for out-of-bounds.\n").
-spec get_cell(buffer(), etui@geometry:position()) -> cell().
get_cell(Buffer, Pos) ->
    case etui@geometry:contains(erlang:element(2, Buffer), Pos) of
        false ->
            empty_cell();

        true ->
            etui_buffer_array_ffi:get(
                pos_to_idx(erlang:element(2, Buffer), Pos),
                erlang:element(3, Buffer)
            )
    end.

-file("src/etui/buffer.gleam", 346).
?DOC(" Set cell at position. Out-of-bounds writes are ignored.\n").
-spec set_cell(buffer(), etui@geometry:position(), cell()) -> buffer().
set_cell(Buffer, Pos, Cell) ->
    case etui@geometry:contains(erlang:element(2, Buffer), Pos) of
        true ->
            {buffer,
                erlang:element(2, Buffer),
                etui_buffer_array_ffi:set(
                    pos_to_idx(erlang:element(2, Buffer), Pos),
                    Cell,
                    erlang:element(3, Buffer)
                )};

        false ->
            Buffer
    end.

-file("src/etui/buffer.gleam", 372).
?DOC(
    " Set cells from a string with an OSC 8 hyperlink URI.\n"
    " Pass `\"\"` for no link (same as `set_string`).\n"
).
-spec set_string_linked(
    buffer(),
    etui@geometry:position(),
    binary(),
    etui@style:color(),
    etui@style:color(),
    etui@style:modifier(),
    binary()
) -> buffer().
set_string_linked(Buffer, Pos, Str, Fg, Bg, Modifier, Link) ->
    case etui@geometry:contains(erlang:element(2, Buffer), Pos) of
        false ->
            Buffer;

        true ->
            Start_idx = pos_to_idx(erlang:element(2, Buffer), Pos),
            Row_end = ((erlang:element(3, Pos) - erlang:element(
                3,
                erlang:element(2, erlang:element(2, Buffer))
            ))
            + 1)
            * erlang:element(2, erlang:element(3, erlang:element(2, Buffer))),
            {buffer,
                erlang:element(2, Buffer),
                etui_buffer_array_ffi:fill_string(
                    erlang:element(3, Buffer),
                    Start_idx,
                    Row_end,
                    Str,
                    Fg,
                    Bg,
                    Modifier,
                    Link
                )}
    end.

-file("src/etui/buffer.gleam", 359).
?DOC(
    " Set cells from a string starting at `pos`. No hyperlink.\n"
    " Wide graphemes (width=2) take one Cell + one Continuation cell.\n"
).
-spec set_string(
    buffer(),
    etui@geometry:position(),
    binary(),
    etui@style:color(),
    etui@style:color(),
    etui@style:modifier()
) -> buffer().
set_string(Buffer, Pos, Str, Fg, Bg, Modifier) ->
    set_string_linked(Buffer, Pos, Str, Fg, Bg, Modifier, <<""/utf8>>).

-file("src/etui/buffer.gleam", 426).
-spec clear_row(buffer(), integer(), integer(), integer()) -> buffer().
clear_row(Buf, Y, X, X_max) ->
    case X >= X_max of
        true ->
            Buf;

        false ->
            Pos = {position, X, Y},
            Cells = case etui@geometry:contains(erlang:element(2, Buf), Pos) of
                true ->
                    etui_buffer_array_ffi:set(
                        pos_to_idx(erlang:element(2, Buf), Pos),
                        empty_cell(),
                        erlang:element(3, Buf)
                    );

                false ->
                    erlang:element(3, Buf)
            end,
            clear_row({buffer, erlang:element(2, Buf), Cells}, Y, X + 1, X_max)
    end.

-file("src/etui/buffer.gleam", 412).
-spec clear_rows(buffer(), integer(), integer(), integer(), integer()) -> buffer().
clear_rows(Buf, Y, Y_max, X_min, X_max) ->
    case Y >= Y_max of
        true ->
            Buf;

        false ->
            clear_rows(
                clear_row(Buf, Y, X_min, X_max),
                Y + 1,
                Y_max,
                X_min,
                X_max
            )
    end.

-file("src/etui/buffer.gleam", 406).
?DOC(" Clear all cells in a rect (reset to empty_cell).\n").
-spec clear(buffer(), etui@geometry:rect()) -> buffer().
clear(Buffer, Rect) ->
    Y_max = etui@geometry:bottom(Rect),
    X_max = etui@geometry:right(Rect),
    clear_rows(
        Buffer,
        erlang:element(3, erlang:element(2, Rect)),
        Y_max,
        erlang:element(2, erlang:element(2, Rect)),
        X_max
    ).

-file("src/etui/buffer.gleam", 456).
-spec buf_view(buffer()) -> buf_view().
buf_view(Buf) ->
    {buf_view,
        erlang:element(3, Buf),
        erlang:element(3, erlang:element(2, erlang:element(2, Buf))),
        erlang:element(2, erlang:element(2, erlang:element(2, Buf))),
        erlang:element(2, erlang:element(3, erlang:element(2, Buf))),
        erlang:element(3, erlang:element(3, erlang:element(2, Buf))),
        erlang:element(2, erlang:element(3, erlang:element(2, Buf))) * erlang:element(
            3,
            erlang:element(3, erlang:element(2, Buf))
        )}.

-file("src/etui/buffer.gleam", 468).
-spec bv_cell_at(buf_view(), integer(), integer()) -> cell().
bv_cell_at(Bv, Row_base, X) ->
    Idx = (Row_base + X) - erlang:element(4, Bv),
    case (Idx >= 0) andalso (Idx < erlang:element(7, Bv)) of
        true ->
            etui_buffer_array_ffi:get(Idx, erlang:element(2, Bv));

        false ->
            empty_cell()
    end.

-file("src/etui/buffer.gleam", 565).
-spec cells_equal(cell(), cell()) -> boolean().
cells_equal(A, B) ->
    A =:= B.

-file("src/etui/buffer.gleam", 537).
-spec collect_run(
    buf_view(),
    buf_view(),
    integer(),
    integer(),
    integer(),
    integer(),
    list(cell())
) -> {list(cell()), integer()}.
collect_run(Prev, Next, Prev_rb, Next_rb, X, X_max, Run) ->
    case X >= X_max of
        true ->
            {lists:reverse(Run), X};

        false ->
            Prev_cell = bv_cell_at(Prev, Prev_rb, X),
            Next_cell = bv_cell_at(Next, Next_rb, X),
            case cells_equal(Prev_cell, Next_cell) of
                true ->
                    {lists:reverse(Run), X};

                false ->
                    collect_run(
                        Prev,
                        Next,
                        Prev_rb,
                        Next_rb,
                        X + 1,
                        X_max,
                        [Next_cell | Run]
                    )
            end
    end.

-file("src/etui/buffer.gleam", 506).
-spec diff_row(
    buf_view(),
    buf_view(),
    integer(),
    integer(),
    integer(),
    integer(),
    integer(),
    list(buffer_op())
) -> list(buffer_op()).
diff_row(Prev, Next, Prev_rb, Next_rb, Y, X, X_max, Rev_acc) ->
    case X >= X_max of
        true ->
            Rev_acc;

        false ->
            Prev_cell = bv_cell_at(Prev, Prev_rb, X),
            Next_cell = bv_cell_at(Next, Next_rb, X),
            case cells_equal(Prev_cell, Next_cell) of
                true ->
                    diff_row(
                        Prev,
                        Next,
                        Prev_rb,
                        Next_rb,
                        Y,
                        X + 1,
                        X_max,
                        Rev_acc
                    );

                false ->
                    Pos = {position, X, Y},
                    {Run, Next_x} = collect_run(
                        Prev,
                        Next,
                        Prev_rb,
                        Next_rb,
                        X,
                        X_max,
                        []
                    ),
                    diff_row(
                        Prev,
                        Next,
                        Prev_rb,
                        Next_rb,
                        Y,
                        Next_x,
                        X_max,
                        [{patch, Pos, Run} | Rev_acc]
                    )
            end
    end.

-file("src/etui/buffer.gleam", 485).
-spec diff_rows(
    buf_view(),
    buf_view(),
    integer(),
    integer(),
    integer(),
    integer(),
    list(buffer_op())
) -> list(buffer_op()).
diff_rows(Prev, Next, Y, Y_max, X_min, X_max, Rev_acc) ->
    case Y >= Y_max of
        true ->
            lists:reverse(Rev_acc);

        false ->
            Prev_rb = (Y - erlang:element(3, Prev)) * erlang:element(5, Prev),
            Next_rb = (Y - erlang:element(3, Next)) * erlang:element(5, Next),
            Rev_acc2 = diff_row(
                Prev,
                Next,
                Prev_rb,
                Next_rb,
                Y,
                X_min,
                X_max,
                Rev_acc
            ),
            diff_rows(Prev, Next, Y + 1, Y_max, X_min, X_max, Rev_acc2)
    end.

-file("src/etui/buffer.gleam", 762).
-spec max_int(integer(), integer()) -> integer().
max_int(A, B) ->
    case A > B of
        true ->
            A;

        false ->
            B
    end.

-file("src/etui/buffer.gleam", 755).
-spec min_int(integer(), integer()) -> integer().
min_int(A, B) ->
    case A < B of
        true ->
            A;

        false ->
            B
    end.

-file("src/etui/buffer.gleam", 477).
?DOC(" Compute minimal diff between two buffers as a list of patches.\n").
-spec diff(buffer(), buffer()) -> list(buffer_op()).
diff(Prev, Next) ->
    Y_min = min_int(
        erlang:element(3, erlang:element(2, erlang:element(2, Prev))),
        erlang:element(3, erlang:element(2, erlang:element(2, Next)))
    ),
    Y_max = max_int(
        etui@geometry:bottom(erlang:element(2, Prev)),
        etui@geometry:bottom(erlang:element(2, Next))
    ),
    X_min = min_int(
        erlang:element(2, erlang:element(2, erlang:element(2, Prev))),
        erlang:element(2, erlang:element(2, erlang:element(2, Next)))
    ),
    X_max = max_int(
        etui@geometry:right(erlang:element(2, Prev)),
        etui@geometry:right(erlang:element(2, Next))
    ),
    diff_rows(buf_view(Prev), buf_view(Next), Y_min, Y_max, X_min, X_max, []).

-file("src/etui/buffer.gleam", 586).
-spec blank_run_style() -> run_style().
blank_run_style() ->
    {run_style, default, default, etui@style:none(), <<""/utf8>>}.

-file("src/etui/buffer.gleam", 595).
-spec run_style_active(run_style()) -> boolean().
run_style_active(Rs) ->
    (((etui@style:ansi_fg(erlang:element(2, Rs)) /= <<""/utf8>>) orelse (etui@style:ansi_bg(
        erlang:element(3, Rs)
    )
    /= <<""/utf8>>))
    orelse (etui@style:ansi_modifier(erlang:element(4, Rs)) /= <<""/utf8>>))
    orelse (erlang:element(5, Rs) /= <<""/utf8>>).

-file("src/etui/buffer.gleam", 740).
-spec osc8_open(binary()) -> binary().
osc8_open(Uri) ->
    <<<<"\x{001B}]8;;"/utf8, Uri/binary>>/binary, "\x{001B}\\"/utf8>>.

-file("src/etui/buffer.gleam", 744).
-spec osc8_close() -> binary().
osc8_close() ->
    <<"\x{001B}]8;;\x{001B}\\"/utf8>>.

-file("src/etui/buffer.gleam", 605).
-spec emit_cell(run_style(), cell()) -> {binary(), run_style()}.
emit_cell(Rs, Cell) ->
    case is_continuation(Cell) of
        true ->
            {<<""/utf8>>, Rs};

        false ->
            Same = (((erlang:element(3, Cell) =:= erlang:element(2, Rs)) andalso (erlang:element(
                4,
                Cell
            )
            =:= erlang:element(3, Rs)))
            andalso (erlang:element(5, Cell) =:= erlang:element(4, Rs)))
            andalso (erlang:element(6, Cell) =:= erlang:element(5, Rs)),
            case Same of
                true ->
                    {cell_symbol(Cell), Rs};

                false ->
                    Link_close = case erlang:element(5, Rs) of
                        <<""/utf8>> ->
                            <<""/utf8>>;

                        _ ->
                            osc8_close()
                    end,
                    Reset_seq = case run_style_active(Rs) of
                        true ->
                            etui@style:ansi_reset();

                        false ->
                            <<""/utf8>>
                    end,
                    Fg_seq = etui@style:ansi_fg(erlang:element(3, Cell)),
                    Bg_seq = etui@style:ansi_bg(erlang:element(4, Cell)),
                    Mod_seq = etui@style:ansi_modifier(erlang:element(5, Cell)),
                    Link_open = case erlang:element(6, Cell) of
                        <<""/utf8>> ->
                            <<""/utf8>>;

                        Uri ->
                            osc8_open(Uri)
                    end,
                    New_rs = {run_style,
                        erlang:element(3, Cell),
                        erlang:element(4, Cell),
                        erlang:element(5, Cell),
                        erlang:element(6, Cell)},
                    {<<<<<<<<<<<<Link_close/binary, Reset_seq/binary>>/binary,
                                            Fg_seq/binary>>/binary,
                                        Bg_seq/binary>>/binary,
                                    Mod_seq/binary>>/binary,
                                Link_open/binary>>/binary,
                            (cell_symbol(Cell))/binary>>,
                        New_rs}
            end
    end.

-file("src/etui/buffer.gleam", 685).
-spec to_ansi_row(buf_view(), integer(), integer(), run_style(), binary()) -> {binary(),
    run_style()}.
to_ansi_row(Bv, Row_base, Col, Rs, Acc) ->
    case Col >= erlang:element(5, Bv) of
        true ->
            {Acc, Rs};

        false ->
            Cell = bv_cell_at(Bv, Row_base, erlang:element(4, Bv) + Col),
            {S, New_rs} = emit_cell(Rs, Cell),
            to_ansi_row(Bv, Row_base, Col + 1, New_rs, <<Acc/binary, S/binary>>)
    end.

-file("src/etui/buffer.gleam", 748).
-spec move_cursor_seq(integer(), integer()) -> binary().
move_cursor_seq(X, Y) ->
    <<<<<<<<"\x{001B}["/utf8, (erlang:integer_to_binary(Y + 1))/binary>>/binary,
                ";"/utf8>>/binary,
            (erlang:integer_to_binary(X + 1))/binary>>/binary,
        "H"/utf8>>.

-file("src/etui/buffer.gleam", 669).
-spec to_ansi_rows(buf_view(), integer(), run_style(), binary()) -> {binary(),
    run_style()}.
to_ansi_rows(Bv, Row, Rs, Acc) ->
    case Row >= erlang:element(6, Bv) of
        true ->
            {Acc, Rs};

        false ->
            Move = move_cursor_seq(
                erlang:element(4, Bv),
                erlang:element(3, Bv) + Row
            ),
            {Row_str, New_rs} = to_ansi_row(
                Bv,
                Row * erlang:element(5, Bv),
                0,
                Rs,
                <<""/utf8>>
            ),
            to_ansi_rows(
                Bv,
                Row + 1,
                New_rs,
                <<<<Acc/binary, Move/binary>>/binary, Row_str/binary>>
            )
    end.

-file("src/etui/buffer.gleam", 659).
?DOC(
    " Full-buffer render to an ANSI string.\n"
    " Emits a MoveCursor for every row, then each cell with style transitions\n"
    " only when the style actually changes between adjacent cells.\n"
    " Use for the first frame or after a terminal resize.\n"
).
-spec to_ansi(buffer()) -> binary().
to_ansi(Buf) ->
    {Output, Final_rs} = to_ansi_rows(
        buf_view(Buf),
        0,
        blank_run_style(),
        <<""/utf8>>
    ),
    Trailing = case run_style_active(Final_rs) of
        true ->
            etui@style:ansi_reset();

        false ->
            <<""/utf8>>
    end,
    <<Output/binary, Trailing/binary>>.

-file("src/etui/buffer.gleam", 707).
?DOC(
    " Convert a list of `BufferOp` patches to an ANSI string.\n"
    " Each patch moves the cursor once, then writes a run of cells.\n"
    " Style is tracked across the entire patch list, cursor moves do not\n"
    " reset terminal style, so we avoid redundant escape sequences.\n"
    " Cheaper than `to_ansi` when only a small fraction of cells changed.\n"
).
-spec patches_to_ansi(list(buffer_op())) -> binary().
patches_to_ansi(Ops) ->
    case Ops of
        [] ->
            <<""/utf8>>;

        _ ->
            {Output, Final_rs} = gleam@list:fold(
                Ops,
                {<<""/utf8>>, blank_run_style()},
                fun(Acc, Op) ->
                    {Str, Rs} = Acc,
                    Move = move_cursor_seq(
                        erlang:element(2, erlang:element(2, Op)),
                        erlang:element(3, erlang:element(2, Op))
                    ),
                    {Cells_str, New_rs} = gleam@list:fold(
                        erlang:element(3, Op),
                        {<<""/utf8>>, Rs},
                        fun(C_acc, Cell) ->
                            {C_str, C_rs} = C_acc,
                            {S, Next_rs} = emit_cell(C_rs, Cell),
                            {<<C_str/binary, S/binary>>, Next_rs}
                        end
                    ),
                    {<<<<Str/binary, Move/binary>>/binary, Cells_str/binary>>,
                        New_rs}
                end
            ),
            Trailing = case run_style_active(Final_rs) of
                true ->
                    etui@style:ansi_reset();

                false ->
                    <<""/utf8>>
            end,
            <<Output/binary, Trailing/binary>>
    end.

-file("src/etui/buffer.gleam", 735).
?DOC(
    " Diff `prev` against `curr` and return the minimal ANSI to bring the\n"
    " terminal from `prev`'s state to `curr`'s state.\n"
    " On the first frame (or after resize) pass an empty buffer as `prev`.\n"
).
-spec diff_to_ansi(buffer(), buffer()) -> binary().
diff_to_ansi(Prev, Curr) ->
    patches_to_ansi(diff(Prev, Curr)).