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