-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, <> ); 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, <>, 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; _ -> <<<>/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 -> <>, 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, _} -> {<>, 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[…`) 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).