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 terminal screen_buffer.ex
Raw

lib/raxol/terminal/screen_buffer.ex

defmodule Raxol.Terminal.ScreenBuffer do
@moduledoc """
Manages the terminal's screen buffer state (grid, scrollback, selection).
This module serves as the main interface for terminal buffer operations,
delegating specific operations to specialized modules in Raxol.Terminal.Buffer.*.
## Structure
The buffer consists of:
* A main grid of cells (the visible screen)
* A scrollback buffer for history
* Selection state
* Scroll region settings
* Dimensions (width and height)
## Operations
The module delegates operations to specialized modules:
* `Content` - Writing and content management
* `ScrollRegion` - Scroll region and scrolling operations
* `LineOperations` - Line manipulation
* `CharEditor` - Character editing
* `LineEditor` - Line editing
* `Eraser` - Clearing operations
* `Selection` - Text selection
* `Scrollback` - History management
* `Queries` - State querying
* `Initializer` - Buffer creation and validation
* `Cursor` - Cursor state management
* `Charset` - Character set management
* `Formatting` - Text formatting and styling
"""
@behaviour Raxol.Terminal.ScreenBufferBehaviour
alias Raxol.Terminal.Cell
alias Raxol.Terminal.ANSI.TextFormatting
alias Raxol.Terminal.Buffer.{
CharEditor,
Writer
}
alias Raxol.Core.Utils.Validation
alias Raxol.Terminal.ScreenBuffer.{
EraseOperations,
MemoryUtils,
RegionOperations,
BehaviourImpl,
Operations,
Attributes
}
defstruct [
:cells,
:scrollback,
:scrollback_limit,
:selection,
:scroll_region,
:scroll_position,
:width,
:height,
:damage_regions,
:default_style,
cursor_position: {0, 0},
cursor_style: :block,
cursor_visible: true,
cursor_blink: true,
alternate_screen: false
]
@type t :: %__MODULE__{
cells: list(list(Cell.t())),
scrollback: list(list(Cell.t())),
scrollback_limit: non_neg_integer(),
selection: {integer(), integer(), integer(), integer()} | nil,
scroll_region: {integer(), integer()} | nil,
scroll_position: non_neg_integer(),
width: non_neg_integer(),
height: non_neg_integer(),
cursor_position: {non_neg_integer(), non_neg_integer()},
cursor_style: atom(),
cursor_visible: boolean(),
cursor_blink: boolean(),
damage_regions: [
{non_neg_integer(), non_neg_integer(), non_neg_integer(),
non_neg_integer()}
],
default_style: TextFormatting.text_style(),
alternate_screen: boolean()
}
# === Core Operations ===
@doc """
Creates a new screen buffer with the specified dimensions.
Validates and normalizes the input dimensions to ensure they are valid.
"""
@impl Raxol.Terminal.ScreenBufferBehaviour
@spec new(integer(), integer()) :: t()
def new(width, height, scrollback_limit \\ 1000) do
width = validate_dimension(width, 80)
height = validate_dimension(height, 24)
scrollback_limit = validate_dimension(scrollback_limit, 1000)
%__MODULE__{
cells: create_empty_grid(width, height),
scrollback: [],
scrollback_limit: scrollback_limit,
selection: nil,
scroll_region: nil,
scroll_position: 0,
width: width,
height: height,
cursor_position: {0, 0},
cursor_style: :block,
cursor_visible: true,
cursor_blink: true,
damage_regions: [],
default_style: TextFormatting.new()
}
end
@spec validate_dimension(integer(), non_neg_integer()) :: non_neg_integer()
defp validate_dimension(dimension, default) do
Validation.validate_dimension(dimension, default)
end
@spec create_empty_grid(non_neg_integer(), non_neg_integer()) ::
list(list(Cell.t()))
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
[]
end
def resize(buffer, new_width, new_height) do
# Validate input dimensions
validate_dimensions(
new_width <= 0 or new_height <= 0,
new_width,
new_height
)
# Create a new ScreenBuffer with the new dimensions
default_cell = %Raxol.Terminal.Cell{
char: " ",
style: nil,
dirty: nil,
wide_placeholder: false
}
# Create new cells array with the new dimensions
new_cells =
List.duplicate(List.duplicate(default_cell, new_width), new_height)
# Copy existing content, truncating or padding as needed
# Handle case where buffer.cells might be nil or corrupted
cells = buffer.cells || []
new_cells =
Enum.reduce(0..min(buffer.height - 1, new_height - 1), new_cells, fn row,
acc ->
row_data = Enum.at(cells, row, [])
Enum.reduce(0..min(buffer.width - 1, new_width - 1), acc, fn col,
row_acc ->
existing_cell =
Enum.at(row_data, col) || default_cell
current_row =
Enum.at(row_acc, row, List.duplicate(default_cell, new_width))
updated_row = List.replace_at(current_row, col, existing_cell)
List.replace_at(row_acc, row, updated_row)
end)
end)
# Clear selection and scroll region after resize
%{
buffer
| width: new_width,
height: new_height,
cells: new_cells,
selection: nil,
scroll_region: nil
}
end
@doc """
Gets all lines from the buffer as a list of lines.
"""
@spec get_lines(t()) :: list(list(Cell.t()))
def get_lines(%__MODULE__{cells: cells}), do: cells
def get_lines(_), do: []
# === Content Operations ===
@spec write_char(t(), non_neg_integer(), non_neg_integer(), String.t()) :: t()
def write_char(buffer, x, y, char) do
write_char(buffer, x, y, char, buffer.default_style)
end
@doc """
Writes a character at the specified position with optional styling.
"""
@impl Raxol.Terminal.ScreenBufferBehaviour
@spec write_char(
t(),
non_neg_integer(),
non_neg_integer(),
String.t(),
TextFormatting.text_style() | nil
) :: t()
def write_char(buffer, x, y, char, style) when x >= 0 and y >= 0 do
case {x < buffer.width, y < buffer.height} do
{true, true} ->
# Use Writer module to handle wide characters properly
Writer.write_char(buffer, x, y, char, style)
_ ->
buffer
end
end
@spec write_string(t(), non_neg_integer(), non_neg_integer(), String.t()) ::
t()
def write_string(buffer, x, y, string),
do: write_string(buffer, x, y, string, nil)
@doc """
Writes a string starting at the specified position.
"""
@impl Raxol.Terminal.ScreenBufferBehaviour
@spec write_string(
t(),
non_neg_integer(),
non_neg_integer(),
String.t(),
TextFormatting.text_style() | nil
) :: t()
def write_string(buffer, x, y, string, style) when x >= 0 and y >= 0 do
case {x < buffer.width, y < buffer.height} do
{true, true} ->
# Use the Writer module which properly handles wide characters
Writer.write_string(buffer, x, y, string, style)
_ ->
buffer
end
end
@impl Raxol.Terminal.ScreenBufferBehaviour
def get_char(buffer, x, y) do
case {x >= 0 and x < buffer.width, y >= 0 and y < buffer.height} do
{true, true} ->
row = Enum.at(buffer.cells, y, [])
cell = Enum.at(row, x)
case cell do
%{char: char} -> char
_ -> " "
end
_ ->
" "
end
end
@doc """
Gets a specific cell from the buffer.
"""
@impl Raxol.Terminal.ScreenBufferBehaviour
@spec get_cell(t(), non_neg_integer(), non_neg_integer()) :: map()
def get_cell(buffer, x, y) when x >= 0 and y >= 0 do
cell =
if x < buffer.width and y < buffer.height do
case buffer.cells do
nil ->
Cell.new()
cells ->
cells
|> Enum.at(y, [])
|> Enum.at(x, Cell.new())
end
else
Cell.new()
end
# Convert Cell struct to map for behaviour compliance
Map.from_struct(cell)
end
def get_cell(_, _, _), do: Map.from_struct(Cell.new())
@doc """
Gets the content of the buffer as a string representation.
"""
@spec get_content(t()) :: String.t()
def get_content(buffer) do
# Convert cells to string representation for compatibility with tests
case buffer.cells do
nil -> ""
cells when is_list(cells) ->
cells
|> Enum.map(fn line when is_list(line) ->
line
|> Enum.map(fn
%Cell{char: char} -> char
cell ->
case Map.get(cell, :char) do
nil -> " "
char -> char
end
end)
|> Enum.join("")
|> String.trim_trailing()
end)
|> Enum.reverse()
|> Enum.drop_while(&(&1 == ""))
|> Enum.reverse()
|> case do
[] -> ""
lines -> Enum.join(lines, "\n")
end
_ -> ""
end
end
@doc """
Puts a line of cells at the specified y position.
"""
@spec put_line(t(), non_neg_integer(), list(Cell.t())) :: t()
def put_line(buffer, y, line) when y >= 0 and y < buffer.height do
new_cells = List.replace_at(buffer.cells || [], y, line)
%{buffer | cells: new_cells}
end
def put_line(buffer, _, _), do: buffer
# === Eraser Operations ===
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate clear_line(buffer, line), to: Operations
defdelegate clear_line(buffer, line, style), to: Operations
defdelegate erase_chars(buffer, count), to: Operations
defdelegate erase_chars(buffer, x, y, count), to: Operations
defdelegate erase_display(buffer, mode), to: Operations
defdelegate erase_line(buffer, mode), to: Operations
defdelegate erase_line(buffer, line, mode), to: Operations
# === Line Operations ===
defdelegate insert_lines(buffer, count), to: Operations
defdelegate delete_lines(buffer, count), to: Operations
# This delegation doesn't exist in Operations yet, keeping original
defdelegate delete_lines_in_region(buffer, lines, y, top, bottom),
to: Raxol.Terminal.Buffer.LineOperations
defdelegate insert_chars(buffer, count), to: Operations
defdelegate delete_chars(buffer, count), to: Operations
defdelegate prepend_lines(buffer, lines), to: Operations
# === Scroll Operations ===
@doc """
Scrolls the buffer content up by the specified number of lines.
Returns {buffer, scrolled_lines} where scrolled_lines are the lines that were scrolled out.
"""
@impl Raxol.Terminal.ScreenBufferBehaviour
@spec scroll_up(t(), non_neg_integer()) :: {t(), list(list(Cell.t()))}
def scroll_up(buffer, lines) when lines > 0 do
{top, bottom} = get_effective_scroll_region(buffer)
if lines > 0 and top < bottom do
# Move lines up within the scroll region
cells = buffer.cells || []
# Extract the region
{before_region, region_and_after} = Enum.split(cells, top)
{region, after_region} = Enum.split(region_and_after, bottom - top + 1)
# Get the lines that will be scrolled out
lines_to_scroll = min(lines, length(region))
scrolled_out = Enum.take(region, lines_to_scroll)
# Scroll the region
remaining_region = Enum.drop(region, lines_to_scroll)
empty_lines =
List.duplicate(create_empty_line(buffer.width), lines_to_scroll)
scrolled_region = remaining_region ++ empty_lines
# Reconstruct the buffer
new_cells = before_region ++ scrolled_region ++ after_region
{%{buffer | cells: new_cells}, scrolled_out}
else
{buffer, []}
end
end
def scroll_up(buffer, _), do: {buffer, []}
@doc """
Scrolls the buffer content down by the specified number of lines.
"""
@impl Raxol.Terminal.ScreenBufferBehaviour
@spec scroll_down(t(), non_neg_integer()) :: t()
def scroll_down(buffer, lines) when lines > 0 do
{top, bottom} = get_effective_scroll_region(buffer)
if lines > 0 and top < bottom do
# Move lines down within the scroll region
cells = buffer.cells || []
# Extract the region
{before_region, region_and_after} = Enum.split(cells, top)
{region, after_region} = Enum.split(region_and_after, bottom - top + 1)
# Scroll the region
lines_to_scroll = min(lines, length(region))
empty_lines =
List.duplicate(create_empty_line(buffer.width), lines_to_scroll)
kept_region = Enum.take(region, length(region) - lines_to_scroll)
scrolled_region = empty_lines ++ kept_region
# Reconstruct the buffer
new_cells = before_region ++ scrolled_region ++ after_region
%{buffer | cells: new_cells}
else
buffer
end
end
def scroll_down(buffer, _), do: buffer
defp create_empty_line(width) when is_integer(width) and width > 0 do
List.duplicate(Cell.new(), width)
end
defp create_empty_line(_width) do
# Return empty list for invalid width
[]
end
defp get_effective_scroll_region(buffer) do
case buffer.scroll_region do
nil -> {0, buffer.height - 1}
{top, bottom} -> {top, min(bottom, buffer.height - 1)}
end
end
# Additional scroll functions for ScrollOperations
def scroll_up(buffer, top, bottom, lines) do
do_scroll_up(buffer, lines, top, bottom)
end
def scroll_down(buffer, top, bottom, lines) do
do_scroll_down(buffer, lines, top, bottom)
end
# Internal scroll implementations
defp do_scroll_up(buffer, lines, top, bottom) do
{effective_top, effective_bottom} =
normalize_scroll_region(buffer, top, bottom)
cells = buffer.cells || []
if effective_top < effective_bottom and lines > 0 do
# Extract the region
{before_region, region_and_after} = Enum.split(cells, effective_top)
{region, after_region} =
Enum.split(region_and_after, effective_bottom - effective_top + 1)
# Scroll the region
lines_to_scroll = min(lines, length(region))
kept_region = Enum.drop(region, lines_to_scroll)
empty_lines =
List.duplicate(create_empty_line(buffer.width), lines_to_scroll)
scrolled_region = kept_region ++ empty_lines
# Reconstruct the buffer
new_cells = before_region ++ scrolled_region ++ after_region
%{buffer | cells: new_cells}
else
buffer
end
end
defp do_scroll_down(buffer, lines, top, bottom) do
{effective_top, effective_bottom} =
normalize_scroll_region(buffer, top, bottom)
cells = buffer.cells || []
if effective_top < effective_bottom and lines > 0 do
# Extract the region
{before_region, region_and_after} = Enum.split(cells, effective_top)
{region, after_region} =
Enum.split(region_and_after, effective_bottom - effective_top + 1)
# Scroll the region
lines_to_scroll = min(lines, length(region))
empty_lines =
List.duplicate(create_empty_line(buffer.width), lines_to_scroll)
kept_region = Enum.take(region, length(region) - lines_to_scroll)
scrolled_region = empty_lines ++ kept_region
# Reconstruct the buffer
new_cells = before_region ++ scrolled_region ++ after_region
%{buffer | cells: new_cells}
else
buffer
end
end
defp normalize_scroll_region(buffer, top, bottom) do
{max(0, top), min(buffer.height - 1, bottom)}
end
def scroll_to(buffer, top, bottom, line) do
# This needs to be implemented or delegated elsewhere
Operations.scroll_to(buffer, top, bottom, line)
end
def reset_scroll_region(buffer) do
clear_scroll_region(buffer)
end
@doc """
Gets the top boundary of the scroll region.
"""
@spec get_scroll_top(t()) :: non_neg_integer()
def get_scroll_top(buffer) do
case buffer.scroll_region do
nil -> 0
{top, _} -> top
end
end
@doc """
Gets the bottom boundary of the scroll region.
"""
@spec get_scroll_bottom(t()) :: non_neg_integer()
def get_scroll_bottom(buffer) do
case buffer.scroll_region do
nil -> buffer.height - 1
{_, bottom} -> bottom
end
end
def set_scroll_region(buffer, {top, bottom}) do
Operations.set_region(buffer, top, bottom)
end
@doc """
Sets the scroll region with individual top and bottom parameters.
"""
@impl Raxol.Terminal.ScreenBufferBehaviour
def set_scroll_region(buffer, top, bottom)
when is_integer(top) and is_integer(bottom) do
Operations.set_region(buffer, top, bottom)
end
# === Dimension Operations ===
@doc """
Gets the dimensions of the buffer.
"""
@impl Raxol.Terminal.ScreenBufferBehaviour
@spec get_dimensions(t()) :: {non_neg_integer(), non_neg_integer()}
def get_dimensions(buffer) do
{buffer.width, buffer.height}
end
@doc """
Gets the width of the buffer.
"""
@impl Raxol.Terminal.ScreenBufferBehaviour
@spec get_width(t()) :: non_neg_integer()
def get_width(buffer) do
buffer.width
end
@doc """
Gets the height of the buffer.
"""
@impl Raxol.Terminal.ScreenBufferBehaviour
@spec get_height(t()) :: non_neg_integer()
def get_height(buffer) do
buffer.height
end
# === Cursor Operations ===
defdelegate set_cursor_position(buffer, x, y), to: Attributes
defdelegate get_cursor_position(buffer), to: Attributes
defdelegate set_cursor_visibility(buffer, visible),
to: Attributes
defdelegate cursor_visible?(buffer), to: Attributes
defdelegate set_cursor_style(buffer, style), to: Attributes
defdelegate get_cursor_style(buffer), to: Attributes
defdelegate set_cursor_blink(buffer, blink), to: Attributes
defdelegate cursor_blinking?(buffer), to: Attributes
# === Charset Operations ===
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate designate_charset(buffer, slot, charset),
to: Attributes
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate get_designated_charset(buffer, slot),
to: Attributes
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate invoke_g_set(buffer, slot), to: Attributes
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate get_current_g_set(buffer), to: Attributes
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate apply_single_shift(buffer, slot), to: Attributes
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate get_single_shift(buffer), to: Attributes
defdelegate reset_charset_state(buffer), to: Attributes
# === Formatting Operations ===
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate get_style(buffer), to: Attributes
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate update_style(buffer, style), to: Attributes
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate set_attribute(buffer, attribute), to: Attributes
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate reset_attribute(buffer, attribute), to: Attributes
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate set_foreground(buffer, color), to: Attributes
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate set_background(buffer, color), to: Attributes
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate reset_all_attributes(buffer), to: Attributes
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate get_foreground(buffer), to: Attributes
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate get_background(buffer), to: Attributes
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate attribute_set?(buffer, attribute), to: Attributes
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate get_set_attributes(buffer), to: Attributes
# === Selection Operations ===
defdelegate start_selection(buffer, x, y), to: Attributes
defdelegate update_selection(buffer, x, y), to: Attributes
defdelegate get_selection(buffer), to: Attributes
defdelegate in_selection?(buffer, x, y), to: Attributes
defdelegate get_selection_boundaries(buffer),
to: Attributes
defdelegate get_text_in_region(buffer, start_x, start_y, end_x, end_y),
to: Attributes
defdelegate clear_selection(buffer), to: Attributes
defdelegate selection_active?(buffer), to: Attributes
defdelegate get_selection_start(buffer),
to: Attributes
defdelegate get_selection_end(buffer), to: Attributes
# === Scroll Region Operations ===
@doc """
Clears the scroll region, resetting to full screen.
"""
@impl Raxol.Terminal.ScreenBufferBehaviour
@spec clear_scroll_region(t()) :: t()
def clear_scroll_region(buffer) do
%{buffer | scroll_region: nil}
end
@doc """
Gets the current scroll region boundaries.
Returns {0, height-1} if no region is set.
"""
@impl Raxol.Terminal.ScreenBufferBehaviour
@spec get_scroll_region_boundaries(t()) ::
{non_neg_integer(), non_neg_integer()}
def get_scroll_region_boundaries(buffer) do
case buffer.scroll_region do
nil -> {0, buffer.height - 1}
{top, bottom} -> {top, bottom}
end
end
# === Query Operations ===
@doc """
Gets a specific line from the buffer.
"""
@spec get_line(t(), non_neg_integer()) :: list(Cell.t())
def get_line(buffer, y) when y >= 0 and y < buffer.height do
case buffer.cells do
nil -> []
cells -> Enum.at(cells, y, [])
end
end
def get_line(_, _), do: []
@doc """
Gets the cell at the specified position in the buffer.
"""
@spec get_cell_at(t(), non_neg_integer(), non_neg_integer()) :: map()
def get_cell_at(buffer, x, y) do
get_cell(buffer, x, y)
end
@doc """
Checks if the buffer is empty.
"""
@impl Raxol.Terminal.ScreenBufferBehaviour
@spec empty?(t()) :: boolean()
def empty?(buffer) do
case buffer.cells do
nil ->
true
cells ->
Enum.all?(cells, fn line ->
Enum.all?(line, &Cell.empty?/1)
end)
end
end
# === Scrollback Operations ===
@doc """
Gets the current scroll position within the scrollback.
"""
@impl Raxol.Terminal.ScreenBufferBehaviour
@spec get_scroll_position(t()) :: non_neg_integer()
def get_scroll_position(buffer) do
buffer.scroll_position || 0
end
# === Cleanup ===
def cleanup(_buffer), do: :ok
# Higher-arity insert_lines for command handlers
@doc """
Inserts blank lines at a specific position with style.
"""
def insert_lines(buffer, y, count, style) do
Operations.insert_lines(buffer, y, count, style)
end
# Higher-arity insert_lines for region
@doc """
Inserts blank lines at a specific position within a region.
"""
def insert_lines(buffer, lines, y, top, bottom) do
Operations.insert_lines(buffer, lines, y, top, bottom)
end
# Higher-arity delete_lines for command handlers
@doc """
Deletes lines at a specific position.
## Parameters
- For command handlers: y, count, style, and region boundaries
- For region operations: lines, y, top, and bottom positions
"""
def delete_lines(buffer, y, count, style, {top, bottom}) do
Operations.delete_lines(
buffer,
y,
count,
style,
{top, bottom}
)
end
# Higher-arity delete_lines for region
def delete_lines(buffer, lines, y, top, bottom) do
Operations.delete_lines(buffer, lines, y, top, bottom)
end
# === Screen Operations ===
def clear(buffer, _style \\ nil) do
# Clear the entire buffer by setting all cells to empty
# Use Cell.new() to match the default cell structure
new_cells = create_empty_grid(buffer.width, buffer.height)
%{buffer | cells: new_cells}
end
def erase_from_cursor_to_end(buffer, x, y, top, bottom) do
try do
EraseOperations.erase_from_cursor_to_end(buffer, x, y, top, bottom)
rescue
_ -> buffer
end
end
def erase_from_start_to_cursor(buffer, x, y, top, bottom) do
try do
EraseOperations.erase_from_start_to_cursor(buffer, x, y, top, bottom)
rescue
_ -> buffer
end
end
@impl Raxol.Terminal.ScreenBufferBehaviour
def erase_all(buffer) do
try do
EraseOperations.erase_all(buffer)
rescue
_ -> buffer
end
end
def clear_region(buffer, x, y, width, height) do
try do
EraseOperations.clear_region(buffer, x, y, width, height)
rescue
_ -> buffer
end
end
def mark_damaged(buffer, x, y, width, height, _reason) do
# Add the new damage region to the existing list
new_region = {x, y, width, height}
updated_damage_regions = [new_region | buffer.damage_regions || []]
%{buffer | damage_regions: updated_damage_regions}
end
def pop_bottom_lines(buffer, count) do
# Remove count lines from bottom and return {buffer, removed_lines}
cells = buffer.cells || []
cells_count = length(cells)
lines_to_remove = min(count, cells_count)
{removed_lines, remaining_cells} = Enum.split(cells, -lines_to_remove)
new_buffer = %{buffer | cells: remaining_cells}
{new_buffer, removed_lines}
end
def erase_display(buffer, mode, _cursor, _min_row, _max_row) do
# Simple implementation for erase display
case mode do
0 -> erase_from_cursor_to_end(buffer)
1 -> erase_from_start_to_cursor(buffer)
2 -> clear(buffer)
_ -> buffer
end
end
@doc """
Erases the entire screen.
"""
@spec erase_screen(t()) :: t()
def erase_screen(buffer) do
case EraseOperations.erase_all(buffer) do
%__MODULE__{} = updated -> updated
_ -> buffer
end
end
def erase_line(buffer, mode, cursor, _min_col, _max_col) do
# Use EraseOperations.erase_in_line which actually exists
{cursor_x, cursor_y} = {elem(cursor, 0), elem(cursor, 1)}
case mode do
0 ->
EraseOperations.erase_in_line(buffer, {cursor_x, cursor_y}, :to_end)
1 ->
EraseOperations.erase_in_line(
buffer,
{cursor_x, cursor_y},
:to_beginning
)
2 ->
EraseOperations.erase_in_line(buffer, {cursor_x, cursor_y}, :all)
_ ->
buffer
end
end
def delete_chars(buffer, count, cursor, _max_col) do
# Simple implementation for delete chars
{cursor_x, cursor_y} = cursor
CharEditor.delete_characters(
buffer,
cursor_y,
cursor_x,
count,
buffer.default_style
)
end
def insert_chars(buffer, _count, _cursor, _max_col) do
# Simple implementation for insert chars - just return buffer for now
buffer
end
def set_dimensions(buffer, width, height) do
resize(buffer, width, height)
end
def get_scrollback(buffer) do
buffer.scrollback || []
end
def set_scrollback(buffer, scrollback) do
%{buffer | scrollback: scrollback}
end
def get_damaged_regions(buffer) do
buffer.damage_regions || []
end
def clear_damaged_regions(buffer) do
%{buffer | damage_regions: []}
end
def get_scroll_region(buffer) do
Operations.get_region(buffer)
end
def shift_region_to_line(buffer, region, target_line) do
try do
Operations.shift_region_to_line(buffer, region, target_line)
rescue
_ -> buffer
end
end
@doc """
Gets the estimated memory usage of the screen buffer.
"""
@spec get_memory_usage(t()) :: non_neg_integer()
defdelegate get_memory_usage(buffer), to: MemoryUtils
def erase_in_line(buffer, position, type) do
try do
EraseOperations.erase_in_line(buffer, position, type)
rescue
_ -> buffer
end
end
def erase_in_display(buffer, position, type) do
EraseOperations.erase_in_display(buffer, position, type)
end
@impl Raxol.Terminal.ScreenBufferBehaviour
def erase_from_cursor_to_end(buffer) do
try do
EraseOperations.erase_from_cursor_to_end(buffer)
rescue
_ -> buffer
end
end
# Higher-arity delete_characters for command handlers
@doc """
Deletes a specified number of characters starting from the given position in the buffer.
Delegates to CharEditor.delete_characters/5.
"""
def delete_characters(buffer, row, col, count, default_style) do
CharEditor.delete_characters(buffer, row, col, count, default_style)
end
@doc """
Scrolls the buffer down by the specified number of lines with additional parameters.
"""
def scroll_down(buffer, lines, count)
when is_integer(lines) and is_integer(count) do
try do
case Raxol.Terminal.Commands.Scrolling.scroll_down(
buffer,
lines,
buffer.scroll_region,
%{}
) do
{updated_buffer, _} -> updated_buffer
updated_buffer -> updated_buffer
end
rescue
_ -> buffer
end
end
# Handle case where lines parameter is a list (from tests)
def scroll_down(buffer, _lines, count) when is_integer(count) do
_ =
Raxol.Terminal.Commands.Scrolling.scroll_down(
buffer,
count,
buffer.scroll_region,
%{}
)
buffer
end
# === Behaviour Callback Implementations ===
# All behaviour implementations delegated to BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate cleanup_file_watching(buffer), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate clear_output_buffer(buffer), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate clear_saved_states(buffer), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate clear_screen(buffer), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate collect_metrics(buffer, type), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate create_chart(buffer, type, options), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate current_theme(), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate enqueue_control_sequence(buffer, sequence), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate erase_all_with_scrollback(buffer), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate erase_from_cursor_to_end_of_line(buffer), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate erase_from_start_of_line_to_cursor(buffer), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate erase_from_start_to_cursor(buffer), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate erase_line(buffer), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate flush_output(buffer), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate get_config(), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate get_current_state(buffer), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate get_metric(buffer, type, name), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate get_metric_value(buffer, name), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate get_metrics_by_type(buffer, type), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate get_output_buffer(buffer), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate get_preferences(), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate get_saved_states_count(buffer), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate get_size(buffer), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate get_state_stack(buffer), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate get_update_settings(), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate handle_csi_sequence(buffer, command, params), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate handle_debounced_events(buffer, events, delay), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate handle_file_event(buffer, event), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate handle_mode(buffer, mode, value), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate has_saved_states?(buffer), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate light_theme(), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate mark_damaged(buffer, x, y, width, height), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate record_metric(buffer, type, name, value), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate record_operation(buffer, operation, duration), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate record_performance(buffer, metric, value), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate record_resource(buffer, type, value), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate reset_state(buffer), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate restore_state(buffer), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate save_state(buffer), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate set_config(config), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate set_preferences(preferences), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate update_current_state(buffer, updates), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate update_state_stack(buffer, stack), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate verify_metrics(buffer, type), to: BehaviourImpl
@impl Raxol.Terminal.ScreenBufferBehaviour
defdelegate write(buffer, data), to: BehaviourImpl
# Overloaded write for compatibility with legacy code
def write(buffer, string, opts) when is_map(buffer) and is_binary(string) do
# Merge opts into the buffer or use separately
write_string(buffer, 0, 0, string, opts[:style] || nil)
end
# === Scroll Operations ===
defdelegate fill_region(buffer, x, y, width, height, cell),
to: RegionOperations
def update(buffer, changes) when is_map(changes) do
Enum.reduce(changes, buffer, fn {key, value}, acc ->
Map.put(acc, key, value)
end)
end
defdelegate handle_single_line_replacement(
lines_list,
row,
start_col,
end_col,
replacement
),
to: RegionOperations
defp validate_dimensions(true, new_width, new_height) do
raise ArgumentError,
"ScreenBuffer dimensions must be positive integers, got: #{new_width}x#{new_height}"
end
defp validate_dimensions(false, _new_width, _new_height), do: :ok
# Functions for compatibility
@doc """
Creates a new buffer with default dimensions.
"""
def new do
new(80, 24)
end
@doc """
Creates a new buffer with a single dimension parameter.
Creates a square buffer.
"""
def new(size) when is_integer(size) and size > 0 do
new(size, size)
end
# Additional compatibility functions for tests and legacy code
@doc """
Scrolls the buffer content.
Returns {buffer, scrolled_lines}.
"""
def scroll(buffer, lines) when lines > 0 do
scroll_up(buffer, lines)
end
def scroll(buffer, lines) when lines < 0 do
# scroll_down doesn't return scrolled_lines, so we return empty list
{scroll_down(buffer, -lines), []}
end
def scroll(buffer, 0), do: {buffer, []}
@doc """
Writes content to the buffer at the specified position.
This is a convenience function for write_string.
"""
def write(buffer, x, y, content) when is_binary(content) do
write_string(buffer, x, y, content)
end
def write(buffer, x, y, content) do
write_char(buffer, x, y, to_string(content))
end
end