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/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
"""
@default_width Raxol.Core.Defaults.terminal_width()
@default_height Raxol.Core.Defaults.terminal_height()
@default_scrollback Raxol.Core.Defaults.scrollback_limit()
@behaviour Raxol.Terminal.ScreenBufferBehaviour
@compile {:no_warn_undefined,
[
Raxol.Terminal.ScreenBuffer.WriteOps,
Raxol.Terminal.ScreenBuffer.ScrollOps,
Raxol.Terminal.ScreenBuffer.EraseOperations,
Raxol.Terminal.ScreenBuffer.LineOps
]}
alias Raxol.Core.Utils.Validation
alias Raxol.Terminal.ANSI.TextFormatting
alias Raxol.Terminal.Cell
alias Raxol.Terminal.ScreenBuffer.{
Attributes,
BehaviourImpl,
EraseOperations,
LineOps,
Operations,
RegionOperations,
ScrollOps,
Selection,
WriteOps
}
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
def new(width, height, scrollback_limit \\ @default_scrollback) do
width = Validation.validate_dimension(width, @default_width)
height = Validation.validate_dimension(height, @default_height)
scrollback_limit = Validation.validate_dimension(scrollback_limit, @default_scrollback)
%__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
def new do
new(@default_width, @default_height)
end
def new(size) when is_integer(size) and size > 0 do
new(size, size)
end
def resize(buffer, new_width, new_height) do
validate_positive_dimensions!(new_width, new_height)
WriteOps.resize(buffer, new_width, new_height)
end
def get_lines(%__MODULE__{cells: cells}), do: cells
def get_lines(_), do: []
# === Content Operations (WriteOps) ===
def write_char(buffer, x, y, char),
do: WriteOps.write_char(buffer, x, y, char)
@impl Raxol.Terminal.ScreenBufferBehaviour
def write_char(buffer, x, y, char, style),
do: WriteOps.write_char(buffer, x, y, char, style)
def write_string(buffer, x, y, string),
do: WriteOps.write_string(buffer, x, y, string)
@impl Raxol.Terminal.ScreenBufferBehaviour
def write_string(buffer, x, y, string, style),
do: WriteOps.write_string(buffer, x, y, string, style)
@impl Raxol.Terminal.ScreenBufferBehaviour
def get_char(buffer, x, y), do: WriteOps.get_char(buffer, x, y)
@impl Raxol.Terminal.ScreenBufferBehaviour
def get_cell(buffer, x, y) when x >= 0 and y >= 0,
do: WriteOps.get_cell(buffer, x, y)
def get_cell(_, _, _), do: WriteOps.get_cell(nil, -1, -1)
def get_content(buffer), do: WriteOps.get_content(buffer)
def put_line(buffer, y, line), do: WriteOps.put_line(buffer, y, line)
# === Eraser Operations (delegated) ===
@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 (delegated) ===
defdelegate insert_lines(buffer, count), to: Operations
defdelegate delete_lines(buffer, count), to: Operations
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 ===
@impl Raxol.Terminal.ScreenBufferBehaviour
def scroll_up(buffer, lines), do: ScrollOps.scroll_up(buffer, lines)
@impl Raxol.Terminal.ScreenBufferBehaviour
def scroll_down(buffer, lines), do: ScrollOps.scroll_down(buffer, lines)
def scroll_up(buffer, top, bottom, lines),
do: ScrollOps.scroll_up(buffer, top, bottom, lines)
def scroll_down(buffer, top, bottom, lines),
do: ScrollOps.scroll_down(buffer, top, bottom, lines)
def scroll_to(buffer, top, bottom, line),
do: ScrollOps.scroll_to(buffer, top, bottom, line)
def reset_scroll_region(buffer),
do: ScrollOps.reset_scroll_region(buffer)
def get_scroll_top(buffer),
do: ScrollOps.get_scroll_top(buffer)
def get_scroll_bottom(buffer),
do: ScrollOps.get_scroll_bottom(buffer)
def set_scroll_region(buffer, {top, bottom}),
do: ScrollOps.set_scroll_region(buffer, {top, bottom})
@impl Raxol.Terminal.ScreenBufferBehaviour
def set_scroll_region(buffer, top, bottom)
when is_integer(top) and is_integer(bottom),
do: ScrollOps.set_scroll_region(buffer, top, bottom)
@impl Raxol.Terminal.ScreenBufferBehaviour
def clear_scroll_region(buffer),
do: ScrollOps.clear_scroll_region(buffer)
@impl Raxol.Terminal.ScreenBufferBehaviour
def get_scroll_region_boundaries(buffer),
do: ScrollOps.get_scroll_region_boundaries(buffer)
@impl Raxol.Terminal.ScreenBufferBehaviour
def get_scroll_position(buffer),
do: ScrollOps.get_scroll_position(buffer)
# === Dimension Operations ===
@impl Raxol.Terminal.ScreenBufferBehaviour
def get_dimensions(buffer), do: {buffer.width, buffer.height}
@impl Raxol.Terminal.ScreenBufferBehaviour
def get_width(buffer), do: buffer.width
@impl Raxol.Terminal.ScreenBufferBehaviour
def get_height(buffer), do: buffer.height
def set_dimensions(buffer, width, height), do: resize(buffer, width, height)
# === 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: Selection
defdelegate clear_selection(buffer), to: Selection
defdelegate get_selected_text(buffer), to: Selection
defdelegate update_selection(buffer, x, y), to: Selection
defdelegate get_selection_boundaries(buffer), to: Selection
defdelegate selection_active?(buffer), to: Selection
defdelegate get_selection_start(buffer), to: Selection
defdelegate get_selection_end(buffer), to: Selection
def get_selection(buffer), do: get_selected_text(buffer)
def in_selection?(buffer, x, y), do: Selection.selected?(buffer, x, y)
defdelegate get_text_in_region(buffer, start_x, start_y, end_x, end_y),
to: Attributes
# === Erase Operations (EraseOps) ===
def clear(buffer, style \\ nil), do: EraseOperations.clear(buffer, style)
def erase_from_cursor_to_end(buffer, x, y, top, bottom),
do: EraseOperations.erase_from_cursor_to_end(buffer, x, y, top, bottom)
def erase_from_start_to_cursor(buffer, x, y, top, bottom),
do: EraseOperations.erase_from_start_to_cursor(buffer, x, y, top, bottom)
@impl Raxol.Terminal.ScreenBufferBehaviour
def erase_all(buffer), do: EraseOperations.erase_all(buffer)
def clear_region(buffer, x, y, width, height),
do: EraseOperations.clear_region(buffer, x, y, width, height)
def erase_display(buffer, mode, cursor, min_row, max_row),
do: EraseOperations.erase_display(buffer, mode, cursor, min_row, max_row)
def erase_screen(buffer), do: EraseOperations.erase_screen(buffer)
def erase_line(buffer, mode, cursor, min_col, max_col),
do: EraseOperations.erase_line(buffer, mode, cursor, min_col, max_col)
def erase_in_line(buffer, position, type),
do: EraseOperations.erase_in_line(buffer, position, type)
def erase_in_display(buffer, position, type),
do: EraseOperations.erase_in_display(buffer, position, type)
@impl Raxol.Terminal.ScreenBufferBehaviour
def erase_from_cursor_to_end(buffer),
do: EraseOperations.erase_from_cursor_to_end(buffer)
def delete_chars(buffer, count, cursor, max_col),
do: EraseOperations.delete_chars(buffer, count, cursor, max_col)
def insert_chars(buffer, count, cursor, max_col),
do: EraseOperations.insert_chars(buffer, count, cursor, max_col)
def delete_characters(buffer, row, col, count, default_style),
do: EraseOperations.delete_characters(buffer, row, col, count, default_style)
# === Line Operations (LineOps) ===
def insert_lines(buffer, y, count, style),
do: LineOps.insert_lines(buffer, y, count, style)
def insert_lines(buffer, y, count, style, {top, bottom}),
do: LineOps.insert_lines(buffer, y, count, style, {top, bottom})
def insert_lines(buffer, lines, y, top, bottom),
do: LineOps.insert_lines_in_region(buffer, lines, y, top, bottom)
def delete_lines(buffer, y, count, style, {top, bottom}),
do: LineOps.delete_lines(buffer, y, count, style, {top, bottom})
def delete_lines(buffer, lines, y, top, bottom),
do: LineOps.delete_lines_in_region(buffer, lines, y, top, bottom)
def pop_bottom_lines(buffer, count),
do: LineOps.pop_bottom_lines(buffer, count)
def get_line(buffer, y), do: LineOps.get_line(buffer, y)
def get_cell_at(buffer, x, y), do: LineOps.get_cell_at(buffer, x, y)
# === Query Operations ===
@impl Raxol.Terminal.ScreenBufferBehaviour
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
def cleanup(_buffer), do: :ok
def mark_damaged(buffer, x, y, width, height, _reason) do
new_region = {x, y, width, height}
updated_damage_regions = [new_region | buffer.damage_regions || []]
%{buffer | damage_regions: updated_damage_regions}
end
# === Scrollback Operations ===
def get_scrollback(buffer), do: buffer.scrollback || []
def set_scrollback(buffer, scrollback),
do: %{buffer | scrollback: scrollback}
def get_damaged_regions(buffer), do: buffer.damage_regions || []
def clear_damaged_regions(buffer), do: %{buffer | damage_regions: []}
def get_scroll_region(buffer),
do: ScrollOps.get_scroll_region(buffer)
def shift_region_to_line(buffer, region, target_line),
do: ScrollOps.shift_region_to_line(buffer, region, target_line)
def scroll_down(buffer, lines, count)
when is_integer(lines) and is_integer(count),
do: ScrollOps.scroll_down_with_count(buffer, lines, count)
def scroll_down(buffer, lines, count) when is_integer(count),
do: ScrollOps.scroll_down_with_count(buffer, lines, count)
# === Behaviour Callback Implementations ===
@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
def write(buffer, string, opts) when is_map(buffer) and is_binary(string) do
write_string(buffer, 0, 0, string, opts[:style] || nil)
end
# === Region Operations ===
defdelegate fill_region(buffer, x, y, width, height, cell),
to: RegionOperations
def update(buffer, changes) when is_map(changes) do
Map.merge(buffer, changes)
end
defdelegate handle_single_line_replacement(
lines_list,
row,
start_col,
end_col,
replacement
),
to: RegionOperations
# === Compatibility Functions ===
def scroll(buffer, lines) when lines > 0, do: scroll_up(buffer, lines)
def scroll(buffer, lines) when lines < 0,
do: {scroll_down(buffer, -lines), []}
def scroll(buffer, 0), do: {buffer, []}
def write(buffer, x, y, content) when is_binary(content),
do: write_string(buffer, x, y, content)
def write(buffer, x, y, content),
do: write_char(buffer, x, y, to_string(content))
# === Private Helpers ===
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: []
defp validate_positive_dimensions!(width, height)
when width <= 0 or height <= 0 do
raise ArgumentError,
"ScreenBuffer dimensions must be positive integers, got: #{width}x#{height}"
end
defp validate_positive_dimensions!(_width, _height), do: :ok
end