-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)).