Packages
Terminal emulation and driver infrastructure for Raxol. ANSI parsing, screen buffers, command processing, cursor management, input handling, session management, and termbox2 NIF integration.
Current section
Files
Jump to
Current section
Files
lib/raxol/terminal/input/input_buffer_utils.ex
defmodule Raxol.Terminal.Input.InputBufferUtils do
@moduledoc """
Utility functions for Raxol.Terminal.Input.InputBuffer.
"""
# --- Wrapping Logic ---
def wrap_line(line, width) do
should_wrap = width > 0 and String.length(line) > width
process_line_wrapping(should_wrap, line, width)
end
defp process_line_wrapping(false, line, _width), do: [line]
defp process_line_wrapping(true, line, width) do
# Split into words
words = String.split(line, " ")
Enum.reduce(words, {[], ""}, fn word, {lines, current_line} ->
process_word(word, width, {lines, current_line})
end)
|> finalize_wrapped_lines()
end
defp finalize_wrapped_lines({lines, last_line}) do
add_final_line(last_line != "", lines, last_line)
end
defp add_final_line(false, lines, _last_line), do: lines
defp add_final_line(true, lines, last_line), do: [last_line | lines]
defp process_word(word, width, {lines, current_line}) do
word_len = String.length(word)
current_line_len = String.length(current_line)
process_word_by_fit(
word,
word_len,
width,
lines,
current_line,
current_line_len
)
end
# Case 1: Word fits perfectly on empty current line (first word)
defp process_word_by_fit(word, word_len, width, lines, "", _current_line_len)
when word_len <= width do
{lines, word}
end
# Case 2: Word fits on current line with a preceding space
defp process_word_by_fit(
word,
word_len,
width,
lines,
current_line,
current_line_len
)
when current_line != "" and current_line_len + 1 + word_len <= width do
{lines, current_line <> " " <> word}
end
# Case 3: Word is too long for any line (longer than width)
defp process_word_by_fit(
word,
word_len,
width,
lines,
current_line,
_current_line_len
)
when word_len > width do
handle_long_word(word, width, lines, current_line)
end
# Case 4: Word doesn't fit on current line, start a new line
defp process_word_by_fit(
word,
_word_len,
_width,
lines,
current_line,
_current_line_len
) do
{[current_line | lines], word}
end
defp handle_long_word(word, width, lines, current_line) do
# Break the long word
{new_lines, remaining_part} = break_long_word(word, width)
# Add the completed current line (if any) and the broken parts
updated_lines = add_current_line(current_line != "", current_line, lines)
# Start new line with remaining part
{new_lines ++ updated_lines, remaining_part}
end
defp add_current_line(false, _current_line, lines), do: lines
defp add_current_line(true, current_line, lines), do: [current_line | lines]
# Private helper for wrap_line
defp break_long_word(word, width) do
graphemes = String.graphemes(word)
parts = Enum.chunk_every(graphemes, width) |> Enum.map(&Enum.join/1)
# The last part might be shorter and becomes the start of the next line
{Enum.slice(parts, 0..-2//1), List.last(parts)}
end
# --- Position Calculation Logic ---
def find_logical_position(contents, cursor_pos) do
logical_lines = String.split(contents, "\n")
char_count = 0
Enum.find_value(Enum.with_index(logical_lines), {0, 0}, fn {line, index} ->
line_len = String.length(line)
# +1 for the newline character, except for the very last line
line_len_with_newline =
line_len + calculate_newline_length(index < length(logical_lines) - 1)
check_cursor_position(
cursor_pos <= char_count + line_len,
index,
cursor_pos,
char_count,
line_len_with_newline
)
end)
end
# Renamed to v2 as it takes the pre-built mapping
def calculate_new_cursor_pos_v2(
# Map of %{old_logical_idx => [new_wrapped_idx1, new_wrapped_idx2...]}
line_mapping,
wrapped_lines_new,
original_logical_line_index,
original_pos_in_line,
# Used for clamping
new_contents
) do
target_wrapped_line_indices =
Map.get(line_mapping, original_logical_line_index, [])
process_cursor_mapping(
target_wrapped_line_indices == [],
new_contents,
target_wrapped_line_indices,
wrapped_lines_new,
original_logical_line_index,
original_pos_in_line
)
end
defp process_cursor_mapping(
true,
new_contents,
_target_wrapped_line_indices,
_wrapped_lines_new,
_original_logical_line_index,
_original_pos_in_line
) do
# Default to end if no mapping found (should indicate an issue)
String.length(new_contents)
end
defp process_cursor_mapping(
false,
new_contents,
target_wrapped_line_indices,
wrapped_lines_new,
_original_logical_line_index,
original_pos_in_line
) do
first_target_wrapped_index = Enum.min(target_wrapped_line_indices)
# Calculate the starting character offset of the *first* wrapped line
# belonging to the original logical line.
start_char_offset =
Enum.reduce(0..(first_target_wrapped_index - 1), 0, fn i, acc ->
# Need to handle potential index out of bounds if mapping is sparse?
line_content = Enum.at(wrapped_lines_new, i)
# +1 for newline implicitly separating wrapped lines
acc + String.length(line_content) + 1
end)
# Iterate through ONLY the target wrapped lines originating from the original logical line
# to find the character offset *within* this sequence of lines.
final_cursor_offset_within_logical_line =
find_cursor_offset_in_target_sequence(
target_wrapped_line_indices,
wrapped_lines_new,
original_pos_in_line
)
# If reduce_while finished without halting (e.g., original_pos_in_line
# was > total length of sequence), default to end of last wrapped line.
final_cursor_offset_within_logical_line =
resolve_cursor_offset(
is_integer(final_cursor_offset_within_logical_line),
final_cursor_offset_within_logical_line,
target_wrapped_line_indices,
wrapped_lines_new
)
final_pos = start_char_offset + final_cursor_offset_within_logical_line
# Clamp to total length as a safety measure
min(final_pos, String.length(new_contents))
end
defp calculate_total_length_of_target_sequence(
target_wrapped_line_indices,
wrapped_lines_new
) do
Enum.reduce(target_wrapped_line_indices, 0, fn idx, acc ->
acc + String.length(Enum.at(wrapped_lines_new, idx))
end)
end
defp find_cursor_offset_in_target_sequence(
target_wrapped_line_indices,
wrapped_lines_new,
original_pos_in_line
) do
Enum.reduce_while(target_wrapped_line_indices, 0, fn wrapped_idx,
pos_within_target_sequence ->
line = Enum.at(wrapped_lines_new, wrapped_idx)
line_len = String.length(line)
handle_cursor_in_line(
original_pos_in_line <= pos_within_target_sequence + line_len,
original_pos_in_line,
pos_within_target_sequence,
line_len
)
end)
end
## Helper Functions for Pattern Matching
defp calculate_newline_length(true), do: 1
defp calculate_newline_length(false), do: 0
defp check_cursor_position(
true,
index,
cursor_pos,
char_count,
_line_len_with_newline
) do
# Cursor is within this logical line
{index, cursor_pos - char_count}
end
defp check_cursor_position(
false,
_index,
_cursor_pos,
char_count,
line_len_with_newline
) do
_char_count = char_count + line_len_with_newline
# Continue searching
nil
end
defp resolve_cursor_offset(
true,
final_cursor_offset_within_logical_line,
_target_wrapped_line_indices,
_wrapped_lines_new
) do
final_cursor_offset_within_logical_line
end
defp resolve_cursor_offset(
false,
_final_cursor_offset_within_logical_line,
target_wrapped_line_indices,
wrapped_lines_new
) do
calculate_total_length_of_target_sequence(
target_wrapped_line_indices,
wrapped_lines_new
)
end
defp handle_cursor_in_line(
true,
original_pos_in_line,
pos_within_target_sequence,
_line_len
) do
# Cursor position falls within this wrapped line. The offset is the
# original position minus the length of preceding lines within sequence.
{:halt, original_pos_in_line - pos_within_target_sequence}
end
defp handle_cursor_in_line(
false,
_original_pos_in_line,
pos_within_target_sequence,
line_len
) do
# Cursor is beyond this wrapped line, continue checking the next one
# from the same logical origin.
{:cont, pos_within_target_sequence + line_len}
end
end