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 erase_operations.ex
Raw

lib/raxol/terminal/screen_buffer/erase_operations.ex

defmodule Raxol.Terminal.ScreenBuffer.EraseOperations do
@moduledoc """
Handles all erase operations for the terminal screen buffer.
This module provides focused functionality for erasing content from the buffer,
including line erasing, display erasing, and region clearing operations.
"""
@default_height Raxol.Core.Defaults.terminal_height()
require Logger
alias Raxol.Terminal.Cell
alias Raxol.Terminal.Buffer.CharEditor
alias Raxol.Terminal.ScreenBuffer
alias Raxol.Terminal.ScreenBuffer.BehaviourImpl
@doc """
Erases from cursor to end of display.
"""
@spec erase_from_cursor_to_end(
map(),
non_neg_integer(),
non_neg_integer(),
non_neg_integer(),
non_neg_integer()
) :: map()
def erase_from_cursor_to_end(buffer, x, y, _top, bottom) do
# Log.info("[DEBUG] erase_from_cursor_to_end called with x=#{x}, y=#{y}, bottom=#{bottom}")
empty_cell = Cell.new()
# Process each line
new_cells =
Enum.with_index(buffer.cells)
|> Enum.map(fn {line, line_idx} ->
cond do
# Lines before cursor row remain unchanged
line_idx < y ->
line
# Cursor row: clear from cursor position to end
line_idx == y ->
preserved_cells = Enum.take(line, x)
cleared_cells = List.duplicate(empty_cell, buffer.width - x)
preserved_cells ++ cleared_cells
# Lines after cursor row: clear entirely if within bottom
line_idx <= bottom ->
List.duplicate(empty_cell, buffer.width)
# Lines beyond bottom remain unchanged
true ->
line
end
end)
%{buffer | cells: new_cells}
end
@doc """
Erases from start to cursor position.
"""
@spec erase_from_start_to_cursor(
map(),
non_neg_integer(),
non_neg_integer(),
non_neg_integer(),
non_neg_integer()
) :: map()
def erase_from_start_to_cursor(buffer, x, y, top, _bottom) do
empty_cell = Cell.new()
# Process each line
new_cells =
Enum.with_index(buffer.cells)
|> Enum.map(fn {line, line_idx} ->
cond do
# Lines before top remain unchanged
line_idx < top ->
line
# Lines from top to before cursor row: clear entirely
line_idx < y ->
List.duplicate(empty_cell, buffer.width)
# Cursor row: clear from start to cursor position (inclusive)
line_idx == y ->
cleared_cells = List.duplicate(empty_cell, x + 1)
preserved_cells = Enum.drop(line, x + 1)
cleared_cells ++ preserved_cells
# Lines after cursor row remain unchanged
true ->
line
end
end)
%{buffer | cells: new_cells}
end
@doc """
Erases the entire buffer.
"""
@spec erase_all(ScreenBuffer.t()) :: ScreenBuffer.t()
def erase_all(buffer) do
empty_cell = Cell.new()
%{
buffer
| cells:
List.duplicate(
List.duplicate(empty_cell, buffer.width),
buffer.height
),
scrollback: []
}
end
@doc """
Clears a specific region of the buffer.
"""
@spec clear_region(
ScreenBuffer.t(),
non_neg_integer(),
non_neg_integer(),
non_neg_integer(),
non_neg_integer()
) :: ScreenBuffer.t()
def clear_region(buffer, x, y, width, height) do
empty_cell = Cell.new()
col_range = x..(x + width - 1)
new_cells =
buffer.cells
|> Enum.with_index()
|> Enum.map(fn {row, row_idx} ->
if row_idx >= y and row_idx < y + height do
clear_columns(row, col_range, empty_cell)
else
row
end
end)
%{buffer | cells: new_cells}
end
defp clear_columns(row, col_range, empty_cell) do
row
|> Enum.with_index()
|> Enum.map(fn {cell, col_idx} ->
if col_idx in col_range, do: empty_cell, else: cell
end)
end
@doc """
Erases part or all of the current line based on the cursor position and type.
Type can be :to_end, :to_beginning, or :all.
"""
@spec erase_in_line(
ScreenBuffer.t(),
{non_neg_integer(), non_neg_integer()},
atom()
) :: ScreenBuffer.t()
def erase_in_line(buffer, {x, y}, type) do
case type do
:to_end -> erase_line_to_end(buffer, x, y)
:to_beginning -> erase_line_to_beginning(buffer, x, y)
:all -> erase_entire_line(buffer, y)
_ -> erase_line_to_end(buffer, x, y)
end
end
@doc """
Erases part or all of the display based on the cursor position and type.
Type can be :to_end, :to_beginning, or :all.
"""
@spec erase_in_display(
ScreenBuffer.t(),
{non_neg_integer(), non_neg_integer()},
atom()
) :: ScreenBuffer.t()
def erase_in_display(buffer, {x, y}, type) do
case type do
:to_end ->
# Erase from cursor to end of display
erase_from_cursor_to_end(buffer, x, y, 0, buffer.height)
:to_beginning ->
# Erase from start of display to cursor
erase_from_start_to_cursor(buffer, x, y, 0, buffer.height)
:all ->
# Erase entire display
erase_all(buffer)
_ ->
# Default to :to_end
erase_in_display(buffer, {x, y}, :to_end)
end
end
@doc """
Erases from the cursor to the end of the screen using the current cursor position.
"""
@spec erase_from_cursor_to_end(ScreenBuffer.t()) :: ScreenBuffer.t()
def erase_from_cursor_to_end(buffer) do
{x, y} = buffer.cursor_position || {0, 0}
height = buffer.height || @default_height
erase_from_cursor_to_end(buffer, x, y, 0, height)
end
# ========================================
# Convenience functions (formerly in EraseOps)
# ========================================
@doc """
Clears the entire buffer, creating a fresh empty grid.
"""
def clear(buffer, _style \\ nil) do
new_cells = create_empty_grid(buffer.width, buffer.height)
%{buffer | cells: new_cells}
end
@doc """
Erases display content based on mode (0=cursor-to-end, 1=start-to-cursor, 2=all).
"""
def erase_display(buffer, mode, _cursor, _min_row, _max_row) do
case mode do
0 -> erase_from_cursor_to_end(buffer)
1 -> BehaviourImpl.erase_from_start_to_cursor(buffer)
2 -> clear(buffer)
_ -> buffer
end
end
@doc """
Erases the entire screen (alias for erase_all).
"""
def erase_screen(buffer), do: erase_all(buffer)
@doc """
Erases line content based on mode (0=cursor-to-end, 1=start-to-cursor, 2=all).
"""
def erase_line(buffer, mode, cursor, _min_col, _max_col) do
{cursor_x, cursor_y} = {elem(cursor, 0), elem(cursor, 1)}
case mode do
0 -> erase_in_line(buffer, {cursor_x, cursor_y}, :to_end)
1 -> erase_in_line(buffer, {cursor_x, cursor_y}, :to_beginning)
2 -> erase_in_line(buffer, {cursor_x, cursor_y}, :all)
_ -> buffer
end
end
@doc """
Deletes characters at cursor position, shifting remaining cells left.
"""
def delete_chars(buffer, count, cursor, _max_col) do
{cursor_x, cursor_y} = cursor
CharEditor.delete_characters(buffer, cursor_y, cursor_x, count, buffer.default_style)
end
@doc """
Inserts blank characters at cursor position (no-op placeholder).
"""
def insert_chars(buffer, _count, _cursor, _max_col), do: buffer
@doc """
Deletes characters at a specific row/col position.
"""
def delete_characters(buffer, row, col, count, default_style) do
CharEditor.delete_characters(buffer, row, col, count, default_style)
end
defp create_empty_grid(width, height) when width > 0 and height > 0 do
for _y <- 0..(height - 1) do
for _x <- 0..(width - 1) do
Cell.new()
end
end
end
defp create_empty_grid(_width, _height), do: []
# Private helper functions
@spec erase_line_to_end(
ScreenBuffer.t(),
non_neg_integer(),
non_neg_integer()
) :: ScreenBuffer.t()
defp erase_line_to_end(buffer, x, y) do
line = Enum.at(buffer.cells, y, [])
empty_cell = Cell.new()
# Preserve cells before cursor, clear from cursor to end
preserved_cells = Enum.take(line, x)
cleared_cells = List.duplicate(empty_cell, buffer.width - x)
cleared_line = preserved_cells ++ cleared_cells
new_cells = List.replace_at(buffer.cells, y, cleared_line)
%{buffer | cells: new_cells}
end
@spec erase_line_to_beginning(
ScreenBuffer.t(),
non_neg_integer(),
non_neg_integer()
) :: ScreenBuffer.t()
defp erase_line_to_beginning(buffer, x, y) do
line = Enum.at(buffer.cells, y, [])
empty_cell = Cell.new()
cleared_line = List.duplicate(empty_cell, x + 1) ++ Enum.drop(line, x + 1)
new_cells = List.replace_at(buffer.cells, y, cleared_line)
%{buffer | cells: new_cells}
end
@spec erase_entire_line(ScreenBuffer.t(), non_neg_integer()) ::
ScreenBuffer.t()
defp erase_entire_line(buffer, y) do
empty_cell = Cell.new()
new_cells =
List.replace_at(buffer.cells, y, List.duplicate(empty_cell, buffer.width))
%{buffer | cells: new_cells}
end
end