-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)}, {<<<<<<<<<<<>/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, <>) 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, <<<>/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, <>. -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), {<>, Next_rs} end ), {<<<>/binary, Cells_str/binary>>, New_rs} end ), Trailing = case run_style_active(Final_rs) of true -> etui@style:ansi_reset(); false -> <<""/utf8>> end, <> 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)).