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
raxol_terminal lib raxol terminal screen_buffer operations.ex
Raw

lib/raxol/terminal/screen_buffer/operations.ex

defmodule Raxol.Terminal.ScreenBuffer.Operations do
@moduledoc """
All buffer mutation operations.
Consolidates: Operations, Ops, OperationsCached, Writer, Updater, CharEditor,
LineOperations, Eraser, Content, Paste functionality.
"""
alias Raxol.Terminal.Cell
alias Raxol.Terminal.CharacterHandling
alias Raxol.Terminal.ScreenBuffer.Core
alias Raxol.Terminal.ScreenBuffer.SharedOperations
@doc """
Writes a character at the specified position.
"""
def write_char(buffer, x, y, char, style \\ nil) do
if Core.within_bounds?(buffer, x, y) do
style = style || buffer.default_style
char_width = CharacterHandling.get_char_width(char)
# Check if we can fit a wide character (need space for placeholder)
can_write_wide = char_width == 2 and Core.within_bounds?(buffer, x + 1, y)
cell = Cell.new(char, style)
new_cells =
List.update_at(buffer.cells, y, fn row ->
write_cell_to_row(row, x, cell, char_width, can_write_wide, style)
end)
damage_width = if char_width == 2 and can_write_wide, do: 2, else: 1
%{
buffer
| cells: new_cells,
damage_regions: add_damage_region(buffer.damage_regions, x, y, damage_width, 1)
}
else
buffer
end
end
@doc """
Writes a sixel graphics character at the specified position with the sixel flag set.
"""
def write_sixel_char(buffer, x, y, char, style \\ nil) do
if Core.within_bounds?(buffer, x, y) do
style = style || buffer.default_style
cell = Raxol.Terminal.Cell.new_sixel(char, style)
new_cells =
List.update_at(buffer.cells, y, fn row ->
List.replace_at(row, x, cell)
end)
%{
buffer
| cells: new_cells,
damage_regions: add_damage_region(buffer.damage_regions, x, y, 1, 1)
}
else
buffer
end
end
@doc """
Writes a string starting at the specified position.
"""
def write_text(buffer, x, y, text, style \\ nil) do
style = style || buffer.default_style
text
|> String.graphemes()
|> Enum.reduce({buffer, x}, fn char, {acc, cur_x} ->
char_w = Raxol.Terminal.CharacterHandling.get_char_width(char)
# Handle line wrapping
if cur_x >= acc.width do
write_y = y + div(cur_x, acc.width)
write_x = rem(cur_x, acc.width)
{write_char(acc, write_x, write_y, char, style), cur_x + char_w}
else
{write_char(acc, cur_x, y, char, style), cur_x + char_w}
end
end)
|> elem(0)
end
@doc """
Inserts a character at the cursor position, shifting content right.
"""
def insert_char(buffer, char, style \\ nil) do
{x, y} = buffer.cursor_position
updated_buffer =
SharedOperations.insert_char_core_logic(buffer, x, y, char, style)
# Add damage tracking if buffer was modified
if updated_buffer != buffer do
%{
updated_buffer
| damage_regions: add_damage_region(buffer.damage_regions, x, y, buffer.width - x, 1)
}
else
buffer
end
end
@doc """
Inserts a character at the specified position.
"""
def insert_char(buffer, x, y, char) do
insert_char(buffer, x, y, char, nil)
end
@doc """
Inserts a character at the specified position with style.
"""
def insert_char(buffer, x, y, char, style) do
updated_buffer =
SharedOperations.insert_char_core_logic(buffer, x, y, char, style)
# Add damage tracking if buffer was modified
if updated_buffer != buffer do
%{
updated_buffer
| damage_regions: add_damage_region(buffer.damage_regions, x, y, buffer.width - x, 1)
}
else
buffer
end
end
@doc """
Writes a string starting at the specified position (alias for write_text).
"""
def write_string(buffer, x, y, string), do: write_text(buffer, x, y, string)
@doc """
Writes a string starting at the specified position with style.
"""
def write_string(buffer, x, y, string, style),
do: write_text(buffer, x, y, string, style)
@doc """
Deletes a character at the cursor position, shifting content left.
"""
def delete_char(buffer) do
{x, y} = buffer.cursor_position
if Core.within_bounds?(buffer, x, y) do
new_cells =
List.update_at(buffer.cells, y, fn row ->
delete_char_from_row(row, x)
end)
%{
buffer
| cells: new_cells,
damage_regions: add_damage_region(buffer.damage_regions, x, y, buffer.width - x, 1)
}
else
buffer
end
end
@doc """
Clears a line.
"""
def clear_line(buffer, y) when y >= 0 and y < buffer.height do
new_cells = List.replace_at(buffer.cells, y, create_empty_row(buffer.width))
%{
buffer
| cells: new_cells,
damage_regions: add_damage_region(buffer.damage_regions, 0, y, buffer.width, 1)
}
end
def clear_line(buffer, _y), do: buffer
@doc """
Clears from cursor to end of line.
"""
def clear_to_end_of_line(buffer) do
{x, y} = buffer.cursor_position
Raxol.Core.Runtime.Log.debug(
"[Operations.clear_to_end_of_line] cursor at (#{x}, #{y}), clearing region (#{x}, #{y}, #{buffer.width - x}, 1)"
)
result = clear_region(buffer, x, y, buffer.width - x, 1)
Raxol.Core.Runtime.Log.debug("[Operations.clear_to_end_of_line] operation complete")
result
end
@doc """
Clears from cursor to beginning of line.
"""
def clear_to_beginning_of_line(buffer) do
{x, y} = buffer.cursor_position
clear_region(buffer, 0, y, x + 1, 1)
end
@doc """
Clears from cursor to end of screen.
"""
def clear_to_end_of_screen(buffer) do
{_x, y} = buffer.cursor_position
# Clear from cursor to end of current line
buffer = clear_to_end_of_line(buffer)
# Clear all lines below
Enum.reduce((y + 1)..(buffer.height - 1), buffer, fn line_y, acc ->
clear_line(acc, line_y)
end)
end
@doc """
Clears from cursor to beginning of screen.
"""
def clear_to_beginning_of_screen(buffer) do
{_x, y} = buffer.cursor_position
# Clear from cursor to beginning of current line
buffer = clear_to_beginning_of_line(buffer)
# Clear all lines above (only if there are lines above)
if y > 0 do
Enum.reduce(0..(y - 1), buffer, fn line_y, acc ->
clear_line(acc, line_y)
end)
else
buffer
end
end
@doc """
Clears a rectangular region.
"""
def clear_region(buffer, x, y, width, height) do
x_end = min(x + width, buffer.width)
y_end = min(y + height, buffer.height)
new_cells =
buffer.cells
|> Enum.with_index()
|> Enum.map(fn {row, row_y} ->
clear_row_region(row, row_y, y, y_end, x, x_end)
end)
%{
buffer
| cells: new_cells,
damage_regions: add_damage_region(buffer.damage_regions, x, y, width, height)
}
end
@doc """
Inserts a blank line at the specified position.
"""
def insert_line(buffer, y) when y >= 0 and y < buffer.height do
empty_line = create_empty_row(buffer.width)
{before, after_} = Enum.split(buffer.cells, y)
new_cells =
before ++ [empty_line] ++ Enum.take(after_, buffer.height - y - 1)
%{
buffer
| cells: new_cells,
damage_regions:
add_damage_region(
buffer.damage_regions,
0,
y,
buffer.width,
buffer.height - y
)
}
end
def insert_line(buffer, _y), do: buffer
@doc """
Deletes a line at the specified position.
"""
def delete_line(buffer, y) when y >= 0 and y < buffer.height do
{before, after_} = Enum.split(buffer.cells, y)
new_cells =
case after_ do
[] -> before ++ [create_empty_row(buffer.width)]
[_ | rest] -> before ++ rest ++ [create_empty_row(buffer.width)]
end
%{
buffer
| cells: new_cells,
damage_regions:
add_damage_region(
buffer.damage_regions,
0,
y,
buffer.width,
buffer.height - y
)
}
end
def delete_line(buffer, _y), do: buffer
@doc """
Fills a region with a character.
"""
def fill_region(buffer, x, y, width, height, char, style \\ nil) do
Enum.reduce(y..(y + height - 1), buffer, fn row_y, acc ->
Enum.reduce(x..(x + width - 1), acc, fn col_x, acc2 ->
write_char(acc2, col_x, row_y, char, style)
end)
end)
end
@doc """
Copies a region to another location.
"""
def copy_region(buffer, src_x, src_y, width, height, dest_x, dest_y) do
# Extract the region
region =
for dy <- 0..(height - 1) do
for dx <- 0..(width - 1) do
Core.get_cell(buffer, src_x + dx, src_y + dy) || Cell.empty()
end
end
# Write the region to destination
region
|> Enum.with_index()
|> Enum.reduce(buffer, fn {row, dy}, acc ->
copy_row_to_dest(row, dy, acc, dest_x, dest_y)
end)
|> Map.update!(
:damage_regions,
&add_damage_region(&1, dest_x, dest_y, width, height)
)
end
# Private helper functions
defp create_empty_row(width) do
List.duplicate(Cell.empty(), width)
end
defp add_damage_region(regions, x, y, width, height) do
new_region = {x, y, x + width - 1, y + height - 1}
merge_damage_regions([new_region | regions])
end
defp merge_damage_regions(regions) do
# Simple implementation - could be optimized
# For now, just keep last 10 regions
Enum.take(regions, 10)
end
@doc """
Puts a line of cells at the specified y position.
Used by scrolling operations and for backward compatibility.
"""
def put_line(buffer, y, line) when y >= 0 and y < buffer.height do
# Ensure line is correct width
line =
cond do
length(line) > buffer.width ->
Enum.take(line, buffer.width)
length(line) < buffer.width ->
line ++ List.duplicate(Cell.new(), buffer.width - length(line))
true ->
line
end
new_cells = List.replace_at(buffer.cells, y, line)
%{
buffer
| cells: new_cells,
damage_regions: add_damage_region(buffer.damage_regions, 0, y, buffer.width, 1)
}
end
def put_line(buffer, _y, _line), do: buffer
# === Stub Implementations for Test Compatibility ===
# These functions are referenced by delegations but not critical for core functionality
@doc """
Clears a line (stub).
"""
def clear_line(buffer, _y, _mode), do: buffer
@doc """
Deletes characters at cursor position.
"""
def delete_chars(buffer, count) do
alias Raxol.Terminal.Buffer.LineOperations
LineOperations.delete_chars(buffer, count)
end
@doc """
Deletes lines (stub with 2 args).
"""
def delete_lines(buffer, _count), do: buffer
@doc """
Deletes lines at position y with count, within a region.
"""
def delete_lines(buffer, y, count, style, {top, bottom}) do
alias Raxol.Terminal.Buffer.LineOperations
# LineOperations expects: (buffer, y, count, top, bottom, style)
LineOperations.delete_lines(buffer, y, count, top, bottom, style)
end
@doc """
Erases characters (stub with 2 args).
"""
def erase_chars(buffer, count) do
{x, y} = buffer.cursor_position
case y < buffer.height and x < buffer.width do
true ->
line = Enum.at(buffer.cells, y, [])
empty_cell = Cell.empty()
# Erase count characters starting at x - delete them and shift content left
{before, after_x} = Enum.split(line, x)
{_erased, remaining} = Enum.split(after_x, count)
# Pad at end to maintain line width
erased_count = length(after_x) - length(remaining)
padding = List.duplicate(empty_cell, erased_count)
new_line = before ++ remaining ++ padding
new_cells = List.replace_at(buffer.cells, y, new_line)
%{
buffer
| cells: new_cells,
damage_regions: add_damage_region(buffer.damage_regions, x, y, count, 1)
}
false ->
buffer
end
end
@doc """
Erases characters at position (stub with 4 args).
"""
def erase_chars(buffer, _x, _y, _count), do: buffer
@doc """
Erases display (stub).
"""
def erase_display(buffer, _mode), do: buffer
@doc """
Erases line (stub with 2 args).
"""
def erase_line(buffer, _mode), do: buffer
@doc """
Erases line at position (stub with 3 args).
"""
def erase_line(buffer, _y, _mode), do: buffer
@doc """
Gets scroll region (stub).
"""
def get_region(%{scroll_region: region}), do: region
def get_region(_buffer), do: nil
@doc """
Inserts spaces at cursor position, shifting content to the right.
Cursor remains at its original position after the operation.
"""
def insert_chars(buffer, count) when count > 0 do
{x, y} = buffer.cursor_position
case Core.within_bounds?(buffer, x, y) do
false ->
buffer
true ->
# Get the current row
row = Enum.at(buffer.cells, y, [])
# Split the row at cursor position
{before_cursor, at_and_after} = Enum.split(row, x)
# Create spaces to insert
spaces =
Enum.map(1..count, fn _ ->
%Cell{char: " ", style: buffer.default_style}
end)
# Reconstruct row: before + spaces + shifted content (truncated to width)
new_row =
(before_cursor ++
spaces ++ Enum.take(at_and_after, buffer.width - x - count))
# Ensure we don't exceed buffer width
|> Enum.take(buffer.width)
# Update cells
new_cells = List.replace_at(buffer.cells, y, new_row)
# Return buffer with updated cells and damage region
# IMPORTANT: Do not update cursor_position - it stays where it was
%{
buffer
| cells: new_cells,
damage_regions:
add_damage_region(
buffer.damage_regions,
x,
y,
buffer.width - x,
1
)
}
end
end
def insert_chars(buffer, _count), do: buffer
@doc """
Inserts lines at cursor position (stub with 2 args).
"""
def insert_lines(buffer, _count), do: buffer
@doc """
Inserts lines at position y with count.
"""
def insert_lines(buffer, y, count, style) do
alias Raxol.Terminal.Buffer.LineOperations
LineOperations.insert_lines(buffer, y, count, style)
end
@doc """
Inserts lines at position y with count, within a scroll region.
"""
def insert_lines(buffer, y, count, style, {top, bottom}) do
alias Raxol.Terminal.Buffer.LineOperations
# LineOperations expects: (buffer, y, count, top, bottom, style)
LineOperations.insert_lines(buffer, y, count, top, bottom, style)
end
@doc """
Prepends lines to buffer.
"""
def prepend_lines(buffer, count) when is_integer(count) do
alias Raxol.Terminal.Buffer.LineOperations
LineOperations.prepend_lines(buffer, count)
end
def prepend_lines(buffer, lines) when is_list(lines) do
alias Raxol.Terminal.Buffer.LineOperations
LineOperations.prepend_lines(buffer, lines)
end
@doc """
Scrolls content down (stub).
"""
def scroll_down(buffer, _count), do: buffer
@doc """
Scrolls content up (stub).
"""
def scroll_up(buffer, _count), do: buffer
@doc """
Scrolls to position (stub).
"""
def scroll_to(buffer, _x, _y, _opts), do: buffer
@doc """
Sets scroll region (stub).
"""
def set_region(buffer, top, bottom) do
%{buffer | scroll_region: {top, bottom}}
end
@doc """
Shifts region content so that target_line appears at the top of the region.
"""
def shift_region_to_line(buffer, {top, bottom}, target_line) do
shift_amount = target_line - top
cond do
shift_amount == 0 ->
%{buffer | scroll_position: target_line}
shift_amount > 0 ->
region_height = bottom - top + 1
lines_to_shift = min(shift_amount, region_height)
new_cells =
Enum.with_index(buffer.cells)
|> Enum.map(fn {line, y} ->
shift_line_in_region(
line,
y,
top,
bottom,
lines_to_shift,
buffer.cells,
buffer.width
)
end)
%{buffer | cells: new_cells, scroll_position: target_line}
true ->
%{buffer | scroll_position: target_line}
end
end
defp write_cell_to_row(row, x, cell, char_width, can_write_wide, style) do
case {char_width, can_write_wide} do
{2, true} ->
# Write wide character + placeholder
placeholder = Cell.new_wide_placeholder(style)
row
|> List.replace_at(x, cell)
|> List.replace_at(x + 1, placeholder)
_ ->
# Write single-width character or wide char at edge
List.replace_at(row, x, cell)
end
end
defp delete_char_from_row(row, x) do
{before, after_} = Enum.split(row, x)
case after_ do
[] -> before
[_ | rest] -> before ++ rest ++ [Cell.empty()]
end
end
defp clear_row_region(row, row_y, y, y_end, x, x_end) do
if row_y >= y and row_y < y_end do
row
|> Enum.with_index()
|> Enum.map(fn {cell, col_x} ->
clear_cell_if_in_range(cell, col_x, x, x_end)
end)
else
row
end
end
defp clear_cell_if_in_range(cell, col_x, x, x_end) do
if col_x >= x and col_x < x_end do
Cell.empty()
else
cell
end
end
defp copy_row_to_dest(row, dy, acc, dest_x, dest_y) do
row
|> Enum.with_index()
|> Enum.reduce(acc, fn {cell, dx}, acc2 ->
copy_cell_to_dest(acc2, cell, dx, dy, dest_x, dest_y)
end)
end
defp copy_cell_to_dest(acc, cell, dx, dy, dest_x, dest_y) do
if Core.within_bounds?(acc, dest_x + dx, dest_y + dy) do
new_cells =
List.update_at(acc.cells, dest_y + dy, fn row ->
List.replace_at(row, dest_x + dx, cell)
end)
%{acc | cells: new_cells}
else
acc
end
end
defp shift_line_in_region(line, y, top, bottom, lines_to_shift, cells, width) do
cond do
y < top or y > bottom ->
line
y <= bottom - lines_to_shift ->
source_y = y + lines_to_shift
Enum.at(cells, source_y, line)
true ->
List.duplicate(%Cell{char: " "}, width)
end
end
end