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 buffer scroll.ex
Raw

lib/raxol/terminal/buffer/scroll.ex

defmodule Raxol.Terminal.Buffer.Scroll do
@moduledoc """
Terminal scroll buffer module.
This module handles the management of terminal scrollback buffers, including:
- Virtual scrolling implementation
- Memory-efficient buffer management
- Scroll position tracking
- Buffer compression
"""
alias Raxol.Terminal.Cell
@type t :: %__MODULE__{
buffer: list(list(Cell.t())),
position: non_neg_integer(),
height: non_neg_integer(),
max_height: non_neg_integer(),
compression_ratio: float(),
memory_limit: non_neg_integer(),
memory_usage: non_neg_integer(),
scroll_region: {non_neg_integer(), non_neg_integer()} | nil
}
defstruct [
:buffer,
:position,
:height,
:max_height,
:compression_ratio,
:memory_limit,
:memory_usage,
scroll_region: nil
]
@doc """
Creates a new scroll buffer with the given dimensions.
## Examples
iex> scroll = Scroll.new(1000)
iex> scroll.max_height
1000
iex> scroll.position
0
"""
def new(max_height, memory_limit \\ 5_000_000) do
%__MODULE__{
buffer: [],
position: 0,
height: 0,
max_height: max_height,
compression_ratio: 1.0,
memory_limit: memory_limit,
memory_usage: 0
}
end
@doc """
Adds a line to the scroll buffer.
## Examples
iex> scroll = Scroll.new(1000)
iex> line = [Cell.new("A"), Cell.new("B")]
iex> scroll = Scroll.add_line(scroll, line)
iex> scroll.height
1
"""
def add_line(%__MODULE__{} = scroll, line) do
new_buffer = [line | scroll.buffer]
# Trim buffer if it exceeds max height
new_buffer =
case length(new_buffer) > scroll.max_height do
true -> Enum.take(new_buffer, scroll.max_height)
false -> new_buffer
end
# Update memory usage and compression if needed
new_usage = calculate_memory_usage(new_buffer)
{new_buffer, new_ratio} =
case new_usage > scroll.memory_limit do
true -> compress_buffer(new_buffer)
false -> {new_buffer, scroll.compression_ratio}
end
%{
scroll
| buffer: new_buffer,
height: length(new_buffer),
compression_ratio: new_ratio,
memory_usage: new_usage
}
end
@doc """
Gets a view of the scroll buffer at the current position.
## Examples
iex> scroll = Scroll.new(1000)
iex> line = [Cell.new("A"), Cell.new("B")]
iex> scroll = Scroll.add_line(scroll, line)
iex> view = Scroll.get_view(scroll, 10)
iex> length(view)
1
"""
def get_view(%__MODULE__{} = scroll, view_height) do
Enum.slice(scroll.buffer, scroll.position, view_height)
end
@doc """
Scrolls the buffer by the given amount.
## Examples
iex> scroll = Scroll.new(1000)
iex> line = [Cell.new("A"), Cell.new("B")]
iex> scroll = Scroll.add_line(scroll, line)
iex> scroll = Scroll.scroll(scroll, 5)
iex> scroll.position
5
"""
def scroll(%__MODULE__{} = scroll, amount) do
new_position =
:erlang.max(0, :erlang.min(scroll.position + amount, scroll.height))
%{scroll | position: new_position}
end
@doc """
Scrolls the buffer in the specified direction by the given amount.
"""
def scroll(%__MODULE__{} = scroll, direction, amount) do
case direction do
:up -> scroll(scroll, -amount)
:down -> scroll(scroll, amount)
_ -> scroll
end
end
@doc """
Gets the current scroll position.
## Examples
iex> scroll = Scroll.new(1000)
iex> Scroll.get_position(scroll)
0
"""
def get_position(%__MODULE__{} = scroll) do
scroll.position
end
@doc """
Gets the total height of the scroll buffer.
## Examples
iex> scroll = Scroll.new(1000)
iex> line = [Cell.new("A"), Cell.new("B")]
iex> scroll = Scroll.add_line(scroll, line)
iex> Scroll.get_height(scroll)
1
"""
def get_height(%__MODULE__{} = scroll) do
scroll.height
end
@doc """
Clears the scroll buffer.
## Examples
iex> scroll = Scroll.new(1000)
iex> line = [Cell.new("A"), Cell.new("B")]
iex> scroll = Scroll.add_line(scroll, line)
iex> scroll = Scroll.clear(scroll)
iex> scroll.height
0
"""
def clear(%__MODULE__{} = scroll) do
%{scroll | buffer: [], position: 0, height: 0, memory_usage: 0}
end
@doc """
Updates the maximum height of the scroll buffer.
Trims the buffer if the new max height is smaller than the current content.
"""
def set_max_height(%__MODULE__{} = scroll, new_max_height)
when is_integer(new_max_height) and new_max_height >= 0 do
new_buffer =
case length(scroll.buffer) > new_max_height do
true -> Enum.take(scroll.buffer, new_max_height)
false -> scroll.buffer
end
new_memory_usage = calculate_memory_usage(new_buffer)
%__MODULE__{
scroll
| buffer: new_buffer,
max_height: new_max_height,
height: length(new_buffer),
# Ensure position is valid
position: min(scroll.position, length(new_buffer) - 1) |> max(0),
memory_usage: new_memory_usage
}
end
@doc """
Gets the visible region of the scroll buffer.
"""
def get_visible_region(%__MODULE__{} = scroll) do
{scroll.position, scroll.position + scroll.height - 1}
end
@doc """
Resizes the scroll buffer to the new height.
"""
def resize(%__MODULE__{} = scroll, new_height) do
%{scroll | height: new_height}
end
@doc """
Gets the size of the scroll buffer.
"""
def get_size(%__MODULE__{} = scroll) do
scroll.height
end
@doc """
Adds content (multiple lines) to the scroll buffer.
"""
def add_content(%__MODULE__{} = scroll, content) when is_list(content) do
Enum.reduce(content, scroll, fn line, acc ->
add_line(acc, line)
end)
end
def add_content(%__MODULE__{} = scroll, _), do: scroll
@doc """
Gets the memory usage of the scroll buffer.
"""
def get_memory_usage(%__MODULE__{} = scroll) do
scroll.memory_usage
end
@doc """
Cleans up the scroll buffer.
"""
def cleanup(%__MODULE__{} = scroll) do
%{scroll | buffer: [], position: 0, height: 0, memory_usage: 0}
end
# Private functions
defp calculate_memory_usage(buffer) do
# Rough estimation of memory usage based on buffer size and content
total_cells =
buffer
|> Enum.map(&length/1)
|> Enum.sum()
# Estimated bytes per cell
cell_size = 100
total_cells * cell_size
end
defp compress_buffer(buffer) do
# Simple compression: merge empty cells and reduce attribute storage
compressed =
buffer
|> Enum.map(&compress_line/1)
# Calculate new compression ratio
original_size = calculate_memory_usage(buffer)
compressed_size = calculate_memory_usage(compressed)
ratio = compressed_size / original_size
{compressed, ratio}
end
defp compress_line(line) do
line
|> Enum.chunk_by(&Cell.empty?/1)
|> Enum.map(&process_cell_chunk/1)
|> List.flatten()
end
defp process_cell_chunk([cell]) do
cell
end
defp process_cell_chunk(cells) do
case Enum.all?(cells, &Cell.empty?/1) do
true -> [List.first(cells)]
false -> Enum.map(cells, &minimize_cell_attributes/1)
end
end
defp minimize_cell_attributes(cell) do
# Access :style, not :attributes
%{cell | style: Map.take(cell.style, [:foreground, :background])}
end
@doc """
Sets the scroll region.
## Parameters
- scroll: The scroll buffer
- top: Top boundary of the scroll region
- bottom: Bottom boundary of the scroll region
## Examples
iex> scroll = Scroll.new(1000)
iex> scroll = Scroll.set_scroll_region(scroll, 1, 5)
iex> scroll.scroll_region
{1, 5}
"""
def set_scroll_region(%__MODULE__{} = scroll, top, bottom)
when is_integer(top) and is_integer(bottom) and top < bottom do
%{scroll | scroll_region: {top, bottom}}
end
def set_scroll_region(%__MODULE__{} = scroll, _top, _bottom) do
scroll
end
@doc """
Clears the scroll region.
## Examples
iex> scroll = Scroll.new(1000)
iex> scroll = Scroll.set_scroll_region(scroll, 1, 5)
iex> scroll = Scroll.clear_scroll_region(scroll)
iex> scroll.scroll_region
nil
"""
def clear_scroll_region(%__MODULE__{} = scroll) do
%{scroll | scroll_region: nil}
end
end