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/manager.ex
defmodule Raxol.Terminal.ScreenBuffer.Manager do
@moduledoc """
Manages buffer lifecycle, memory tracking, damage regions, and buffer switching.
Consolidates: Manager, UnifiedManager, SafeManager, EnhancedManager, DamageTracker.
"""
@default_scrollback Raxol.Core.Defaults.scrollback_limit()
alias Raxol.Terminal.ScreenBuffer.Core
defstruct [
:main_buffer,
:alternate_buffer,
:active_buffer_type,
:memory_limit,
:memory_usage,
:metrics
]
@type t :: %__MODULE__{
main_buffer: Core.t(),
alternate_buffer: Core.t(),
active_buffer_type: :main | :alternate,
memory_limit: non_neg_integer(),
memory_usage: non_neg_integer(),
metrics: map()
}
@doc """
Creates a new buffer manager with main and alternate buffers.
"""
@spec new(non_neg_integer(), non_neg_integer(), keyword()) :: t()
def new(width, height, opts \\ []) do
# 10MB default
memory_limit = Keyword.get(opts, :memory_limit, 10_000_000)
scrollback_limit = Keyword.get(opts, :scrollback_limit, @default_scrollback)
main = Core.new(width, height, scrollback_limit)
# No scrollback for alternate
alternate = Core.new(width, height, 0)
%__MODULE__{
main_buffer: main,
alternate_buffer: alternate,
active_buffer_type: :main,
memory_limit: memory_limit,
memory_usage: calculate_memory_usage(main, alternate),
metrics: %{
writes: 0,
scrolls: 0,
clears: 0,
switches: 0
}
}
end
@doc """
Gets the currently active buffer.
"""
@spec get_active_buffer(t()) :: Core.t()
def get_active_buffer(manager) do
case manager.active_buffer_type do
:main -> manager.main_buffer
:alternate -> manager.alternate_buffer
end
end
@doc """
Updates the active buffer.
Can accept either:
- A function that transforms the current buffer
- A new buffer to replace the current one
"""
@spec update_active_buffer(t(), (Core.t() -> Core.t())) :: t()
def update_active_buffer(manager, fun) when is_function(fun, 1) do
active = get_active_buffer(manager)
updated = fun.(active)
manager =
case manager.active_buffer_type do
:main -> %{manager | main_buffer: updated}
:alternate -> %{manager | alternate_buffer: updated}
end
update_memory_usage(manager)
end
@spec update_active_buffer(t(), Core.t()) :: t()
def update_active_buffer(manager, buffer) when is_struct(buffer, Core) do
manager =
case manager.active_buffer_type do
:main -> %{manager | main_buffer: buffer}
:alternate -> %{manager | alternate_buffer: buffer}
end
update_memory_usage(manager)
end
@doc """
Switches between main and alternate buffers.
"""
@spec switch_buffer(t(), :main | :alternate) :: t()
def switch_buffer(manager, :main) do
%{
manager
| active_buffer_type: :main,
metrics: increment_metric(manager.metrics, :switches)
}
end
def switch_buffer(manager, :alternate) do
# Save cursor position when switching to alternate
main_with_saved_cursor =
Map.put(
manager.main_buffer,
:saved_cursor_for_main,
manager.main_buffer.cursor_position
)
%{
manager
| main_buffer: main_with_saved_cursor,
active_buffer_type: :alternate,
metrics: increment_metric(manager.metrics, :switches)
}
end
@doc """
Toggles between main and alternate buffers.
"""
@spec toggle_buffer(t()) :: t()
def toggle_buffer(manager) do
new_type =
case manager.active_buffer_type do
:main -> :alternate
:alternate -> :main
end
switch_buffer(manager, new_type)
end
@doc """
Switches to alternate buffer (convenience function).
"""
@spec switch_to_alternate(t()) :: t()
def switch_to_alternate(manager) do
switch_buffer(manager, :alternate)
end
@doc """
Switches to main buffer (convenience function).
"""
@spec switch_to_main(t()) :: t()
def switch_to_main(manager) do
switch_buffer(manager, :main)
end
# Damage tracking
@doc """
Adds a damage region to the active buffer.
"""
@spec add_damage(
t(),
non_neg_integer(),
non_neg_integer(),
non_neg_integer(),
non_neg_integer()
) :: t()
def add_damage(manager, x, y, width, height) do
update_active_buffer(manager, fn buffer ->
new_region = {x, y, x + width - 1, y + height - 1}
existing = buffer.damage_regions || []
merged = merge_damage_regions([new_region | existing])
%{buffer | damage_regions: merged}
end)
end
@doc """
Gets all damage regions from the active buffer.
"""
@spec get_damage_regions(t()) :: list(tuple())
def get_damage_regions(manager) do
buffer = get_active_buffer(manager)
buffer.damage_regions
end
@doc """
Clears all damage regions from the active buffer.
"""
@spec clear_damage_regions(t()) :: t()
def clear_damage_regions(manager) do
update_active_buffer(manager, fn buffer ->
%{buffer | damage_regions: []}
end)
end
@doc """
Marks the entire buffer as damaged.
"""
@spec mark_all_damaged(t()) :: t()
def mark_all_damaged(manager) do
buffer = get_active_buffer(manager)
add_damage(manager, 0, 0, buffer.width, buffer.height)
end
# Memory management
@doc """
Updates memory usage calculation.
"""
@spec update_memory_usage(t()) :: t()
def update_memory_usage(manager) do
usage =
calculate_memory_usage(manager.main_buffer, manager.alternate_buffer)
%{manager | memory_usage: usage}
end
@doc """
Checks if within memory limits.
"""
@spec within_memory_limits?(t()) :: boolean()
def within_memory_limits?(manager) do
manager.memory_usage <= manager.memory_limit
end
@doc """
Gets memory usage statistics.
"""
@spec get_memory_stats(t()) :: map()
def get_memory_stats(manager) do
%{
usage: manager.memory_usage,
limit: manager.memory_limit,
percentage: Float.round(manager.memory_usage / manager.memory_limit * 100, 2),
main_buffer_size: estimate_buffer_size(manager.main_buffer),
alternate_buffer_size: estimate_buffer_size(manager.alternate_buffer)
}
end
@doc """
Gets current memory usage in bytes.
"""
@spec get_memory_usage(t()) :: non_neg_integer()
def get_memory_usage(manager) do
manager.memory_usage
end
@doc """
Trims scrollback if exceeding memory limits.
"""
@spec trim_if_needed(t()) :: t()
def trim_if_needed(manager) do
if within_memory_limits?(manager) do
manager
else
# Trim scrollback from main buffer
main = %{
manager.main_buffer
| scrollback:
Enum.take(
manager.main_buffer.scrollback,
div(manager.main_buffer.scrollback_limit, 2)
)
}
%{manager | main_buffer: main} |> update_memory_usage()
end
end
# Metrics
@doc """
Increments a write operation metric.
"""
@spec record_write(t()) :: t()
def record_write(manager) do
%{manager | metrics: increment_metric(manager.metrics, :writes)}
end
@doc """
Increments a scroll operation metric.
"""
@spec record_scroll(t()) :: t()
def record_scroll(manager) do
%{manager | metrics: increment_metric(manager.metrics, :scrolls)}
end
@doc """
Increments a clear operation metric.
"""
@spec record_clear(t()) :: t()
def record_clear(manager) do
%{manager | metrics: increment_metric(manager.metrics, :clears)}
end
@doc """
Gets all metrics.
"""
@spec get_metrics(t()) :: map()
def get_metrics(manager) do
manager.metrics
end
@doc """
Resets metrics.
"""
@spec reset_metrics(t()) :: t()
def reset_metrics(manager) do
%{manager | metrics: %{writes: 0, scrolls: 0, clears: 0, switches: 0}}
end
# Buffer operations forwarding
@doc """
Resizes both buffers.
"""
@spec resize(t(), non_neg_integer(), non_neg_integer()) :: t()
def resize(manager, new_width, new_height) do
%{
manager
| main_buffer: Core.resize(manager.main_buffer, new_width, new_height),
alternate_buffer: Core.resize(manager.alternate_buffer, new_width, new_height)
}
|> mark_all_damaged()
|> update_memory_usage()
end
@doc """
Clears the active buffer.
"""
@spec clear(t()) :: t()
def clear(manager) do
manager
|> update_active_buffer(&Core.clear/1)
|> record_clear()
|> mark_all_damaged()
end
# Private helper functions
defp calculate_memory_usage(main_buffer, alternate_buffer) do
# overhead
estimate_buffer_size(main_buffer) + estimate_buffer_size(alternate_buffer) +
1000
end
defp estimate_buffer_size(buffer) do
# ~8 bytes per cell
cells_size = buffer.width * buffer.height * 8
scrollback_size = length(buffer.scrollback || []) * buffer.width * 8
cells_size + scrollback_size
end
defp merge_damage_regions(regions) do
# Simple implementation - could be optimized to actually merge overlapping regions
# For now, just keep the last 10 regions
Enum.take(regions, 10)
end
defp increment_metric(metrics, key) do
Map.update(metrics, key, 1, &(&1 + 1))
end
# === Stub Implementations for Test Compatibility ===
# These functions are referenced by test helpers but not critical for core functionality
@doc """
Writes data to the active buffer (stub for test compatibility).
"""
@spec write(t(), binary(), keyword()) :: {:ok, t()} | t()
def write(manager, data, _opts \\ []) when is_binary(data) do
# Simple stub - just return the manager unchanged
# In a real implementation, this would write to the buffer
{:ok, manager}
end
@doc """
Reads data from the active buffer (stub for test compatibility).
"""
@spec read(t(), keyword()) :: binary()
def read(_manager, _opts \\ []) do
# Return empty content for now
# In a real implementation, this would read from the buffer
""
end
@doc """
Initializes buffers (stub for test compatibility).
"""
@spec initialize_buffers(
non_neg_integer(),
non_neg_integer(),
non_neg_integer()
) :: t()
def initialize_buffers(width, height, scrollback_limit \\ @default_scrollback) do
new(width, height, scrollback_limit: scrollback_limit)
end
@doc """
Starts a GenServer for the manager (stub for test compatibility).
"""
@spec start_link(keyword()) :: {:ok, pid()} | {:error, term()}
def start_link(_opts \\ []) do
# For tests that expect a process, we start a minimal agent
Agent.start_link(fn -> %{} end)
end
@doc """
Constrains a position to buffer bounds (stub).
"""
@spec constrain_position(t(), integer(), integer()) :: {integer(), integer()}
def constrain_position(manager, x, y) do
buffer = get_active_buffer(manager)
x = max(0, min(x, buffer.width - 1))
y = max(0, min(y, buffer.height - 1))
{x, y}
end
@doc """
Gets current cursor position (stub).
"""
@spec get_position(t()) :: {integer(), integer()}
def get_position(manager) do
buffer = get_active_buffer(manager)
buffer.cursor_position
end
@doc """
Moves cursor to position (stub).
"""
@spec move_to(t(), integer(), integer()) :: t()
def move_to(manager, x, y) do
update_active_buffer(manager, fn buffer ->
{x, y} = constrain_position(manager, x, y)
%{buffer | cursor_position: {x, y}}
end)
end
@doc """
Resets cursor position to origin (stub).
"""
@spec reset_position(t()) :: t()
def reset_position(manager) do
move_to(manager, 0, 0)
end
@doc """
Updates cursor position with delta (stub).
"""
@spec update_position(t(), {integer(), integer()}) :: t()
def update_position(manager, {dx, dy}) do
{x, y} = get_position(manager)
move_to(manager, x + dx, y + dy)
end
@doc """
Gets total lines in buffer including scrollback (stub).
"""
@spec get_total_lines(t()) :: non_neg_integer()
def get_total_lines(manager) do
buffer = get_active_buffer(manager)
length(buffer.cells) + length(buffer.scrollback)
end
@doc """
Gets visible lines count (stub).
"""
@spec get_visible_lines(t()) :: non_neg_integer()
def get_visible_lines(manager) do
buffer = get_active_buffer(manager)
buffer.height
end
@doc """
Gets visible content as string (stub).
"""
@spec get_visible_content(t()) :: String.t()
def get_visible_content(manager) do
buffer = get_active_buffer(manager)
# Return empty string for now - real implementation would render cells
String.duplicate(" ", buffer.width * buffer.height)
end
@doc """
Updates visible region for scrolling (stub).
"""
@spec update_visible_region(t(), non_neg_integer()) :: t()
def update_visible_region(manager, scroll_offset) do
update_active_buffer(manager, fn buffer ->
%{buffer | scroll_position: scroll_offset}
end)
end
@doc """
Clears damage regions (stub).
"""
@spec clear_damage(t()) :: t()
def clear_damage(manager) do
update_active_buffer(manager, fn buffer ->
%{buffer | damage_regions: []}
end)
end
end