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.
"""
import Raxol.Guards
@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 binary?(line) and 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 binary?(line) and 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, current_line) do
if current_line == "",
do: Enum.reverse(lines),
else: Enum.reverse([String.trim(current_line) | lines])
end
defp do_wrap_words([word | rest], width, lines, current_line) do
new_line =
if current_line == "", do: word, else: current_line <> " " <> word
cond do
String.length(word) > width ->
handle_long_word(word, rest, width, lines, current_line)
String.length(new_line) <= width ->
# Word fits on the current line
do_wrap_words(rest, width, lines, new_line)
true ->
# Word doesn't fit, start a new line
do_wrap_words(
rest,
width,
[String.trim(current_line) | lines],
word
)
end
end
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 =
if current_line == "" do
lines
else
[String.trim(current_line) | lines]
end
# 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
# do_wrap_words end
end
# Module end