-module(etui@text).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/etui/text.gleam").
-export([graphemes/1, codepoint_cell_width/1, grapheme_cell_width/1, cell_width/1, truncate/3, wrap/2, pad_right/2, pad_left/2, align/3, strip_ansi/1]).
-export_type([alignment/0, strip_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.
-type alignment() :: left | center | right.
-type strip_state() :: plain | esc_seen | csi_body | osc_body | osc_esc_seen.
-file("src/etui/text.gleam", 33).
?DOC(
" Split a string into grapheme clusters (UAX #29, via Erlang Unicode).\n"
"\n"
" Each element is one user-perceived character: a base letter, a ZWJ\n"
" sequence, a flag pair, an emoji with modifiers, etc.\n"
"\n"
" ```gleam\n"
" graphemes(\"café\") // [\"c\", \"a\", \"f\", \"é\"]\n"
" graphemes(\"👨👩👧👦\") // [\"👨👩👧👦\"], one cluster\n"
" ```\n"
).
-spec graphemes(binary()) -> list(binary()).
graphemes(S) ->
gleam@string:to_graphemes(S).
-file("src/etui/text.gleam", 58).
?DOC(
" Cell width of a single Unicode codepoint.\n"
" Returns 0 for control / combining / zero-width, 2 for wide (CJK / emoji /\n"
" fullwidth), 1 otherwise.\n"
).
-spec codepoint_cell_width(integer()) -> integer().
codepoint_cell_width(Cp) ->
case Cp of
N when N < 16#20 ->
0;
16#7F ->
0;
N@1 when (N@1 >= 16#0300) andalso (N@1 =< 16#036F) ->
0;
N@2 when (N@2 >= 16#1160) andalso (N@2 =< 16#11FF) ->
0;
N@3 when (N@3 >= 16#FE00) andalso (N@3 =< 16#FE0F) ->
0;
N@4 when (N@4 >= 16#E0100) andalso (N@4 =< 16#E01EF) ->
0;
16#200B ->
0;
16#200C ->
0;
16#200D ->
0;
16#FEFF ->
0;
N@5 when (N@5 >= 16#1100) andalso (N@5 =< 16#115F) ->
2;
N@6 when (N@6 >= 16#2E80) andalso (N@6 =< 16#303E) ->
2;
N@7 when (N@7 >= 16#3041) andalso (N@7 =< 16#33FF) ->
2;
N@8 when (N@8 >= 16#3400) andalso (N@8 =< 16#4DBF) ->
2;
N@9 when (N@9 >= 16#4E00) andalso (N@9 =< 16#9FFF) ->
2;
N@10 when (N@10 >= 16#A000) andalso (N@10 =< 16#A4CF) ->
2;
N@11 when (N@11 >= 16#AC00) andalso (N@11 =< 16#D7A3) ->
2;
N@12 when (N@12 >= 16#F900) andalso (N@12 =< 16#FAFF) ->
2;
N@13 when (N@13 >= 16#FE30) andalso (N@13 =< 16#FE4F) ->
2;
N@14 when (N@14 >= 16#FF00) andalso (N@14 =< 16#FF60) ->
2;
N@15 when (N@15 >= 16#FFE0) andalso (N@15 =< 16#FFE6) ->
2;
N@16 when (N@16 >= 16#1F1E6) andalso (N@16 =< 16#1F1FF) ->
2;
N@17 when (N@17 >= 16#1F300) andalso (N@17 =< 16#1F5FF) ->
2;
N@18 when (N@18 >= 16#1F600) andalso (N@18 =< 16#1F64F) ->
2;
N@19 when (N@19 >= 16#1F680) andalso (N@19 =< 16#1F6FF) ->
2;
N@20 when (N@20 >= 16#1F700) andalso (N@20 =< 16#1F77F) ->
2;
N@21 when (N@21 >= 16#1F780) andalso (N@21 =< 16#1F7FF) ->
2;
N@22 when (N@22 >= 16#1F800) andalso (N@22 =< 16#1F8FF) ->
2;
N@23 when (N@23 >= 16#1F900) andalso (N@23 =< 16#1F9FF) ->
2;
N@24 when (N@24 >= 16#1FA00) andalso (N@24 =< 16#1FAFF) ->
2;
N@25 when (N@25 >= 16#20000) andalso (N@25 =< 16#2FFFD) ->
2;
N@26 when (N@26 >= 16#30000) andalso (N@26 =< 16#3FFFD) ->
2;
_ ->
1
end.
-file("src/etui/text.gleam", 48).
?DOC(
" Cell width of a single grapheme cluster.\n"
" Uses the first codepoint's East Asian Width / emoji classification.\n"
" Subsequent codepoints in a grapheme (combining, ZWJ, variation selectors)\n"
" contribute 0, so the first determines the visible cell count.\n"
).
-spec grapheme_cell_width(binary()) -> integer().
grapheme_cell_width(G) ->
case gleam@string:to_utf_codepoints(G) of
[] ->
0;
[Cp | _] ->
codepoint_cell_width(gleam_stdlib:identity(Cp))
end.
-file("src/etui/text.gleam", 38).
?DOC(" Cell width of a string (sum of grapheme widths).\n").
-spec cell_width(binary()) -> integer().
cell_width(S) ->
_pipe = S,
_pipe@1 = gleam@string:to_graphemes(_pipe),
gleam@list:fold(_pipe@1, 0, fun(Acc, G) -> Acc + grapheme_cell_width(G) end).
-file("src/etui/text.gleam", 159).
-spec take_prefix(list(binary()), integer(), integer(), binary()) -> binary().
take_prefix(Graphemes, Available, Width, Acc) ->
case Graphemes of
[] ->
Acc;
[G | Rest] ->
G_width = grapheme_cell_width(G),
case (Width + G_width) =< Available of
true ->
take_prefix(
Rest,
Available,
Width + G_width,
<<Acc/binary, G/binary>>
);
false ->
Acc
end
end.
-file("src/etui/text.gleam", 141).
?DOC(
" Truncate to max_width cells. Appends ellipsis only if truncation occurs.\n"
" The ellipsis itself counts toward the budget.\n"
).
-spec truncate(binary(), integer(), binary()) -> binary().
truncate(S, Max_width, Ellipsis) ->
case Max_width of
W when W =< 0 ->
<<""/utf8>>;
_ ->
S_width = cell_width(S),
case S_width =< Max_width of
true ->
S;
false ->
Ellipsis_width = cell_width(Ellipsis),
Available = gleam@int:max(0, Max_width - Ellipsis_width),
Gs = gleam@string:to_graphemes(S),
<<(take_prefix(Gs, Available, 0, <<""/utf8>>))/binary,
Ellipsis/binary>>
end
end.
-file("src/etui/text.gleam", 250).
-spec hard_break_acc(
list(binary()),
integer(),
integer(),
binary(),
list(binary())
) -> list(binary()).
hard_break_acc(Gs, Max_width, Curr_w, Curr, Acc) ->
case Gs of
[] ->
case Curr of
<<""/utf8>> ->
Acc;
_ ->
lists:append(Acc, [Curr])
end;
[G | Rest] ->
Gw = grapheme_cell_width(G),
case (Curr_w > 0) andalso ((Curr_w + Gw) > Max_width) of
true ->
hard_break_acc(
Rest,
Max_width,
Gw,
G,
lists:append(Acc, [Curr])
);
false ->
hard_break_acc(
Rest,
Max_width,
Curr_w + Gw,
<<Curr/binary, G/binary>>,
Acc
)
end
end.
-file("src/etui/text.gleam", 246).
-spec hard_break_word(binary(), integer()) -> list(binary()).
hard_break_word(S, Max_width) ->
hard_break_acc(gleam@string:to_graphemes(S), Max_width, 0, <<""/utf8>>, []).
-file("src/etui/text.gleam", 196).
-spec wrap_para_words(binary(), integer()) -> list(binary()).
wrap_para_words(S, Max_width) ->
Words = gleam@string:split(S, <<" "/utf8>>),
{Rev_lines, Curr} = gleam@list:fold(
Words,
{[], <<""/utf8>>},
fun(Acc, Word) ->
{Lines_acc, Curr_line} = Acc,
W_width = cell_width(Word),
Curr_width = cell_width(Curr_line),
Space_w = case Curr_line of
<<""/utf8>> ->
0;
_ ->
1
end,
case ((Curr_width + Space_w) + W_width) =< Max_width of
true ->
New_line = case Curr_line of
<<""/utf8>> ->
Word;
_ ->
<<<<Curr_line/binary, " "/utf8>>/binary,
Word/binary>>
end,
{Lines_acc, New_line};
false ->
Lines2 = case Curr_line of
<<""/utf8>> ->
Lines_acc;
_ ->
[Curr_line | Lines_acc]
end,
case W_width =< Max_width of
true ->
{Lines2, Word};
false ->
Chunks = hard_break_word(Word, Max_width),
case lists:reverse(Chunks) of
[] ->
{Lines2, <<""/utf8>>};
[Last | Rest_rev] ->
{lists:append(Rest_rev, Lines2), Last}
end
end
end
end
),
All_rev = case Curr of
<<""/utf8>> ->
Rev_lines;
_ ->
[Curr | Rev_lines]
end,
lists:reverse(All_rev).
-file("src/etui/text.gleam", 188).
-spec wrap_para(binary(), integer()) -> list(binary()).
wrap_para(S, Max_width) ->
case S of
<<""/utf8>> ->
[<<""/utf8>>];
_ ->
wrap_para_words(S, Max_width)
end.
-file("src/etui/text.gleam", 179).
?DOC(
" Word-wrap to max_width cells. Handles explicit `\\n` newlines.\n"
" Returns list of lines, each padded to max_width cells.\n"
).
-spec wrap(binary(), integer()) -> list(binary()).
wrap(S, Max_width) ->
case Max_width of
W when W =< 0 ->
[];
_ ->
_pipe = gleam@string:split(S, <<"\n"/utf8>>),
gleam@list:flat_map(
_pipe,
fun(Para) -> wrap_para(Para, Max_width) end
)
end.
-file("src/etui/text.gleam", 275).
?DOC(" Pad right with spaces to reach `width` cells. Cell-aware.\n").
-spec pad_right(binary(), integer()) -> binary().
pad_right(S, Width) ->
Cw = cell_width(S),
case Cw >= Width of
true ->
S;
false ->
<<S/binary, (gleam@string:repeat(<<" "/utf8>>, Width - Cw))/binary>>
end.
-file("src/etui/text.gleam", 284).
?DOC(" Pad left with spaces to reach `width` cells. Cell-aware.\n").
-spec pad_left(binary(), integer()) -> binary().
pad_left(S, Width) ->
Cw = cell_width(S),
case Cw >= Width of
true ->
S;
false ->
<<(gleam@string:repeat(<<" "/utf8>>, Width - Cw))/binary, S/binary>>
end.
-file("src/etui/text.gleam", 293).
?DOC(" Align left/center/right within `width` cells. Cell-aware.\n").
-spec align(binary(), integer(), alignment()) -> binary().
align(S, Width, Alignment) ->
case Alignment of
left ->
pad_right(S, Width);
right ->
pad_left(S, Width);
center ->
Cw = cell_width(S),
case Cw >= Width of
true ->
S;
false ->
Total = Width - Cw,
Left = Total div 2,
Right = Total - Left,
<<<<(gleam@string:repeat(<<" "/utf8>>, Left))/binary,
S/binary>>/binary,
(gleam@string:repeat(<<" "/utf8>>, Right))/binary>>
end
end.
-file("src/etui/text.gleam", 359).
-spec is_csi_final(binary()) -> boolean().
is_csi_final(G) ->
case gleam@string:to_utf_codepoints(G) of
[Cp | _] ->
N = gleam_stdlib:identity(Cp),
(N >= 16#40) andalso (N =< 16#7E);
[] ->
false
end.
-file("src/etui/text.gleam", 329).
-spec strip_loop(list(binary()), binary(), strip_state()) -> binary().
strip_loop(Gs, Acc, State) ->
case Gs of
[] ->
Acc;
[G | Rest] ->
{New_acc, New_state} = case {State, G} of
{plain, <<"\x{001B}"/utf8>>} ->
{Acc, esc_seen};
{plain, _} ->
{<<Acc/binary, G/binary>>, plain};
{esc_seen, <<"["/utf8>>} ->
{Acc, csi_body};
{esc_seen, <<"]"/utf8>>} ->
{Acc, osc_body};
{esc_seen, _} ->
{Acc, plain};
{csi_body, C} ->
case is_csi_final(C) of
true ->
{Acc, plain};
false ->
{Acc, csi_body}
end;
{osc_body, <<"\x{0007}"/utf8>>} ->
{Acc, plain};
{osc_body, <<"\x{001B}"/utf8>>} ->
{Acc, osc_esc_seen};
{osc_body, _} ->
{Acc, osc_body};
{osc_esc_seen, <<"\\"/utf8>>} ->
{Acc, plain};
{osc_esc_seen, _} ->
{Acc, osc_body}
end,
strip_loop(Rest, New_acc, New_state)
end.
-file("src/etui/text.gleam", 317).
?DOC(
" Strip ANSI escape sequences. Handles CSI (`\\e[…<final>`) and OSC\n"
" (`\\e]…ST`/`\\e]…BEL`) sequences.\n"
).
-spec strip_ansi(binary()) -> binary().
strip_ansi(S) ->
strip_loop(gleam@string:to_graphemes(S), <<""/utf8>>, plain).