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/buffer/concurrent_buffer.ex
defmodule Raxol.Terminal.Buffer.ConcurrentBuffer do
@moduledoc """
A thread-safe buffer implementation using GenServer for concurrent access.
Provides synchronous operations to ensure data integrity when multiple
processes are reading/writing to the buffer simultaneously.
"""
use Raxol.Core.Behaviours.BaseManager
alias Raxol.Terminal.Buffer
alias Raxol.Terminal.Buffer.Cell
# Client API
@doc """
Starts a concurrent buffer server.
Options:
- :width - Buffer width (default: 80)
- :height - Buffer height (default: 24)
- :name - GenServer name (optional)
"""
@spec start_server(keyword()) :: {:ok, pid()} | {:error, term()}
def start_server(opts \\ []) do
start_link(opts)
end
@doc """
Stops the concurrent buffer server.
"""
@spec stop(pid() | atom()) :: :ok
def stop(server) do
GenServer.stop(server, :normal)
end
@doc """
Sets a cell in the buffer.
"""
@spec set_cell(pid() | atom(), integer(), integer(), Cell.t()) ::
:ok | {:error, term()}
def set_cell(server, x, y, cell) do
GenServer.call(server, {:set_cell, x, y, cell})
end
@doc """
Gets a cell from the buffer.
"""
@spec get_cell(pid() | atom(), integer(), integer()) ::
{:ok, Cell.t()} | {:error, term()}
def get_cell(server, x, y) do
GenServer.call(server, {:get_cell, x, y})
end
@doc """
Writes text starting at the given position.
"""
@spec write(pid() | atom(), integer(), integer(), String.t(), map()) ::
:ok | {:error, term()}
def write(server, x, y, text, style \\ %{}) do
GenServer.call(server, {:write, x, y, text, style})
end
@doc """
Clears the entire buffer.
"""
@spec clear(pid() | atom()) :: :ok
def clear(server) do
GenServer.call(server, :clear)
end
@doc """
Fills a region with a character.
"""
@spec fill_region(
pid() | atom(),
integer(),
integer(),
integer(),
integer(),
String.t(),
map()
) :: :ok
def fill_region(server, x, y, width, height, char, style \\ %{}) do
GenServer.call(server, {:fill_region, x, y, width, height, char, style})
end
@doc """
Scrolls the buffer content.
"""
@spec scroll(pid() | atom(), integer()) :: :ok
def scroll(server, lines) do
GenServer.call(server, {:scroll, lines})
end
@doc """
Flushes any pending operations (for compatibility).
Returns :ok immediately as operations are synchronous.
"""
@spec flush(pid() | atom()) :: :ok
def flush(_server), do: :ok
@doc """
Gets the current buffer state for reading.
"""
@spec get_buffer(pid() | atom()) :: {:ok, Buffer.t()} | {:error, term()}
def get_buffer(server) do
GenServer.call(server, :get_buffer)
end
@doc """
Performs a batch of operations atomically.
"""
@spec batch(pid() | atom(), (Buffer.t() -> Buffer.t())) ::
:ok | {:error, term()}
def batch(server, fun) when is_function(fun, 1) do
GenServer.call(server, {:batch, fun})
end
@doc """
Performs a batch of operations from a list.
"""
@spec batch_operations(pid() | atom(), list()) :: :ok | {:error, term()}
def batch_operations(server, operations) when is_list(operations) do
GenServer.call(server, {:batch_operations, operations})
end
# Server callbacks
@impl true
def init_manager(opts) do
width = Keyword.get(opts, :width, 80)
height = Keyword.get(opts, :height, 24)
state = %{
buffer: Buffer.new({width, height}),
width: width,
height: height
}
{:ok, state}
end
@impl true
def handle_manager_call({:set_cell, x, y, cell}, _from, state) do
case validate_coords(x, y, state) do
:ok ->
# Update the buffer with the new cell
updated_buffer = Buffer.set_cell(state.buffer, x, y, cell)
{:reply, :ok, %{state | buffer: updated_buffer}}
{:error, _} = error ->
{:reply, error, state}
end
rescue
e -> {:reply, {:error, e}, state}
end
@impl true
def handle_manager_call({:get_cell, x, y}, _from, state) do
case validate_coords(x, y, state) do
:ok ->
cell = Buffer.get_cell(state.buffer, x, y)
{:reply, {:ok, cell}, state}
{:error, _} = error ->
{:reply, error, state}
end
rescue
e -> {:reply, {:error, e}, state}
end
@impl true
def handle_manager_call({:write, x, y, text, style}, _from, state) do
case validate_coords(x, y, state) do
:ok ->
updated_buffer = write_text(state.buffer, x, y, text, style)
{:reply, :ok, %{state | buffer: updated_buffer}}
{:error, _} = error ->
{:reply, error, state}
end
rescue
e -> {:reply, {:error, e}, state}
end
@impl true
def handle_manager_call(:clear, _from, state) do
cleared_buffer = Buffer.new({state.width, state.height})
{:reply, :ok, %{state | buffer: cleared_buffer}}
end
@impl true
def handle_manager_call(
{:fill_region, x, y, width, height, char, style},
_from,
state
) do
case validate_region(x, y, width, height, state) do
:ok ->
updated_buffer =
fill_buffer_region(state.buffer, x, y, width, height, char, style)
{:reply, :ok, %{state | buffer: updated_buffer}}
{:error, _} = error ->
{:reply, error, state}
end
rescue
e -> {:reply, {:error, e}, state}
end
@impl true
def handle_manager_call({:scroll, lines}, _from, state) do
# Simplified scroll implementation
# In a real implementation, this would shift buffer content
updated_buffer = perform_scroll(state.buffer, lines, state.height)
{:reply, :ok, %{state | buffer: updated_buffer}}
rescue
e -> {:reply, {:error, e}, state}
end
@impl true
def handle_manager_call(:get_buffer, _from, state) do
{:reply, {:ok, state.buffer}, state}
end
@impl true
def handle_manager_call({:batch, fun}, _from, state) do
updated_buffer = fun.(state.buffer)
{:reply, :ok, %{state | buffer: updated_buffer}}
rescue
e -> {:reply, {:error, e}, state}
end
@impl true
def handle_manager_call({:batch_operations, operations}, _from, state) do
updated_buffer = apply_batch_operations(operations, state.buffer, state)
{:reply, :ok, %{state | buffer: updated_buffer}}
rescue
e -> {:reply, {:error, e}, state}
end
# Private helpers
defp apply_batch_operations(operations, buffer, state) do
Enum.reduce(operations, buffer, fn operation, acc_buffer ->
case operation do
{:set_cell, x, y, cell} ->
apply_set_cell_operation(acc_buffer, x, y, cell, state)
{:write_string, x, y, text} ->
apply_write_string_operation(acc_buffer, x, y, text, state)
{:fill_region, x, y, width, height, cell} ->
apply_fill_region_operation(
acc_buffer,
x,
y,
width,
height,
cell,
state
)
_ ->
acc_buffer
end
end)
end
defp fill_buffer_region_with_cell(buffer, x, y, width, height, cell) do
Enum.reduce(y..(y + height - 1), buffer, fn row, acc_buffer ->
Enum.reduce(x..(x + width - 1), acc_buffer, fn col, inner_acc ->
Buffer.set_cell(inner_acc, col, row, cell)
end)
end)
end
defp validate_coords(x, y, %{width: width, height: height}) do
cond do
x < 0 or x >= width ->
{:error, :out_of_bounds}
y < 0 or y >= height ->
{:error, :out_of_bounds}
true ->
:ok
end
end
defp validate_region(x, y, width, height, state) do
cond do
x < 0 or y < 0 or width < 0 or height < 0 ->
{:error, :invalid_region}
x + width > state.width or y + height > state.height ->
{:error, :out_of_bounds}
true ->
:ok
end
end
defp write_text(buffer, x, y, text, style) do
text
|> String.graphemes()
|> Enum.with_index()
|> Enum.reduce(buffer, fn {char, offset}, acc ->
cell =
if map_size(style) > 0 do
Cell.new(char, style)
else
Cell.new(char: char)
end
Buffer.set_cell(acc, x + offset, y, cell)
end)
end
defp fill_buffer_region(buffer, x, y, width, height, char, style) do
Enum.reduce(y..(y + height - 1), buffer, fn row, acc_buffer ->
Enum.reduce(x..(x + width - 1), acc_buffer, fn col, inner_acc ->
cell = create_cell_with_style(char, style)
Buffer.set_cell(inner_acc, col, row, cell)
end)
end)
end
defp perform_scroll(buffer, lines, _height) when lines > 0 do
# Scroll up: move content up, add blank lines at bottom
# Simplified implementation - in practice would preserve content
buffer
end
defp perform_scroll(buffer, lines, _height) when lines < 0 do
# Scroll down: move content down, add blank lines at top
# Simplified implementation - in practice would preserve content
buffer
end
defp perform_scroll(buffer, _lines, _height), do: buffer
defp apply_set_cell_operation(buffer, x, y, cell, state) do
case validate_coords(x, y, state) do
:ok -> Buffer.set_cell(buffer, x, y, cell)
{:error, _} -> buffer
end
end
defp apply_write_string_operation(buffer, x, y, text, state) do
case validate_coords(x, y, state) do
:ok -> write_text(buffer, x, y, text, %{})
{:error, _} -> buffer
end
end
defp apply_fill_region_operation(buffer, x, y, width, height, cell, state) do
case validate_region(x, y, width, height, state) do
:ok -> fill_buffer_region_with_cell(buffer, x, y, width, height, cell)
{:error, _} -> buffer
end
end
defp create_cell_with_style(char, style) do
case map_size(style) > 0 do
true -> Cell.new(char, style)
false -> Cell.new(char: char)
end
end
end