Packages

Multi-surface application runtime for Elixir. One TEA module renders to terminal, browser (LiveView), SSH, and MCP (agents). 30+ widgets, flexbox + CSS grid, AI agent runtime, distributed swarm with CRDTs, time-travel debugging, session recording, sandboxed REPL, and agentic commerce.

Current section

Files

Jump to
raxol lib raxol ui components input text_wrapping.ex
Raw

lib/raxol/ui/components/input/text_wrapping.ex

defmodule Raxol.UI.Components.Input.TextWrapping do
@moduledoc """
Utility functions for text wrapping.
"""
@doc """
Wraps a single line of text by character count using recursion.
Handles multi-byte characters correctly.
"""
def wrap_line_by_char(line, width)
when is_binary(line) and is_integer(width) and width > 0 do
do_wrap_char(String.graphemes(line), width, [])
end
# Recursive helper for wrap_line_by_char
# Base case: No more graphemes left, accumulator is empty (empty input)
defp do_wrap_char([], _width, []) do
[""]
end
# Base case: No more graphemes left, return reversed accumulator
defp do_wrap_char([], _width, acc) do
Enum.reverse(acc)
end
# Recursive step
defp do_wrap_char(graphemes, width, acc) do
chunk_graphemes = Enum.take(graphemes, width)
rest_graphemes = Enum.drop(graphemes, width)
chunk_string = Enum.join(chunk_graphemes)
do_wrap_char(rest_graphemes, width, [chunk_string | acc])
end
@doc """
Wraps a single line of text by word boundaries.
"""
def wrap_line_by_word(line, width)
when is_binary(line) and is_integer(width) and width > 0 do
words = String.split(line, " ")
do_wrap_words(words, width, [], "")
end
# Private helper for wrap_line_by_word
defp do_wrap_words([], _width, lines, ""), do: Enum.reverse(lines)
defp do_wrap_words([], _width, lines, current_line) do
Enum.reverse([String.trim(current_line) | lines])
end
defp do_wrap_words([word | rest], width, lines, current_line) do
new_line = build_new_line(current_line, word)
case categorize_word_fit(word, new_line, width) do
:word_too_long ->
handle_long_word(word, rest, width, lines, current_line)
:fits_current_line ->
# Word fits on the current line
do_wrap_words(rest, width, lines, new_line)
:needs_new_line ->
# Word doesn't fit, start a new line
do_wrap_words(
rest,
width,
[String.trim(current_line) | lines],
word
)
end
end
defp categorize_word_fit(word, new_line, width) do
word_length = String.length(word)
new_line_length = String.length(new_line)
case {word_length > width, new_line_length <= width} do
{true, _} -> :word_too_long
{false, true} -> :fits_current_line
{false, false} -> :needs_new_line
end
end
defp build_new_line("", word), do: word
defp build_new_line(current_line, word), do: current_line <> " " <> word
defp handle_long_word(word, rest, width, lines, current_line) do
# 1. Finalize the current line (if it's not empty) and add it to lines.
finalized_lines = finalize_current_line(current_line, lines)
# 2. Wrap the long word by character.
wrapped_word_parts = wrap_line_by_char(word, width)
# 3. The last part of the wrapped word becomes the start of the *next* current_line.
# The preceding parts are added to the accumulated lines (in reverse order for prepending).
case Enum.reverse(wrapped_word_parts) do
[] ->
# This case should only happen if wrap_line_by_char gets an empty string or width <= 0,
# which is guarded against, but handle defensively.
do_wrap_words(rest, width, finalized_lines, "")
[last_part | initial_parts_rev] ->
# Prepend already reversed initial parts
updated_lines = initial_parts_rev ++ finalized_lines
# 4. Recurse with the rest of the words, using the last part of the wrapped word
# as the new current line, and the updated lines accumulator.
do_wrap_words(rest, width, updated_lines, last_part)
end
end
defp finalize_current_line("", lines), do: lines
defp finalize_current_line(current_line, lines) do
[String.trim(current_line) | lines]
end
# do_wrap_words end
end
# Module end