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/split/split_manager.ex
defmodule Raxol.Terminal.Split.SplitManager do
@moduledoc """
Manages terminal split windows and panes.
Each split can optionally be bound to a `ConcurrentBuffer` pid and a
`TerminalProcess` pid, enabling the cockpit to map panes to live
terminal buffers.
"""
use Raxol.Core.Behaviours.BaseManager
@default_dimensions %{width: 80, height: 24}
@default_position %{x: 0, y: 0}
defstruct [
:id,
:dimensions,
:position,
:content,
:created_at,
:buffer_pid,
:terminal_pid,
:label
]
@type t :: %__MODULE__{
id: integer(),
dimensions: %{width: integer(), height: integer()},
position: %{x: integer(), y: integer()},
content: map(),
created_at: DateTime.t(),
buffer_pid: pid() | nil,
terminal_pid: pid() | nil,
label: String.t() | nil
}
# Client API
# BaseManager provides start_link
@doc """
Creates a new split with the given options.
"""
@spec create_split(map(), pid()) :: {:ok, t()} | {:error, term()}
def create_split(opts \\ %{}, pid) do
GenServer.call(pid, {:create_split, opts})
end
@doc """
Resizes an existing split.
"""
@spec resize_split(integer(), %{width: integer(), height: integer()}, pid()) ::
{:ok, t()} | {:error, :not_found}
def resize_split(split_id, new_dimensions, pid) do
GenServer.call(pid, {:resize_split, split_id, new_dimensions})
end
@doc """
Navigates to an existing split.
"""
@spec navigate_to_split(integer(), pid()) :: {:ok, t()} | {:error, :not_found}
def navigate_to_split(split_id, pid) do
GenServer.call(pid, {:navigate_to_split, split_id})
end
@doc """
Lists all splits.
"""
@spec list_splits(pid()) :: [t()]
def list_splits(pid) do
GenServer.call(pid, :list_splits)
end
@doc """
Binds a ConcurrentBuffer and optional TerminalProcess to a split.
"""
@spec bind_buffer(integer(), pid(), pid(), pid() | nil) ::
{:ok, t()} | {:error, :not_found}
def bind_buffer(split_id, manager_pid, buffer_pid, terminal_pid \\ nil) do
GenServer.call(
manager_pid,
{:bind_buffer, split_id, buffer_pid, terminal_pid}
)
end
@doc """
Unbinds the buffer from a split.
"""
@spec unbind_buffer(integer(), pid()) :: {:ok, t()} | {:error, :not_found}
def unbind_buffer(split_id, manager_pid) do
GenServer.call(manager_pid, {:unbind_buffer, split_id})
end
@doc """
Gets the buffer pid bound to a split.
"""
@spec get_split_buffer(integer(), pid()) ::
{:ok, pid()} | {:error, :not_found | :no_buffer}
def get_split_buffer(split_id, manager_pid) do
GenServer.call(manager_pid, {:get_split_buffer, split_id})
end
@doc """
Sets a label on a split (for display in pane headers).
"""
@spec set_label(integer(), String.t(), pid()) ::
{:ok, t()} | {:error, :not_found}
def set_label(split_id, label, manager_pid) do
GenServer.call(manager_pid, {:set_label, split_id, label})
end
@doc """
Removes a split by id.
"""
@spec remove_split(integer(), pid()) :: :ok | {:error, :not_found}
def remove_split(split_id, manager_pid) do
GenServer.call(manager_pid, {:remove_split, split_id})
end
# Server callbacks
@impl true
def init_manager(state) when is_list(state) do
# Convert list to map for keyword list options
init_manager(Map.new(state))
end
def init_manager(state) when is_map(state) do
# Ensure state has required fields
initial_state = %{
splits: Map.get(state, :splits, %{}),
next_id: Map.get(state, :next_id, 1)
}
{:ok, initial_state}
end
@impl true
def handle_manager_call({:create_split, opts}, _from, state) do
split_id = state.next_id
dimensions = opts[:dimensions] || @default_dimensions
position = opts[:position] || @default_position
split = %__MODULE__{
id: split_id,
dimensions: dimensions,
position: position,
content: %{},
created_at: DateTime.utc_now(),
buffer_pid: opts[:buffer_pid],
terminal_pid: opts[:terminal_pid],
label: opts[:label]
}
new_state = %{
state
| splits: Map.put(state.splits, split_id, split),
next_id: split_id + 1
}
{:reply, {:ok, split}, new_state}
end
@impl true
def handle_manager_call(
{:resize_split, split_id, new_dimensions},
_from,
state
) do
case Map.get(state.splits, split_id) do
nil ->
{:reply, {:error, :not_found}, state}
split ->
updated_split = %{split | dimensions: new_dimensions}
new_state = %{
state
| splits: Map.put(state.splits, split_id, updated_split)
}
{:reply, {:ok, updated_split}, new_state}
end
end
@impl true
def handle_manager_call({:navigate_to_split, split_id}, _from, state) do
case Map.get(state.splits, split_id) do
nil ->
{:reply, {:error, :not_found}, state}
split ->
{:reply, {:ok, split}, state}
end
end
@impl true
def handle_manager_call(:list_splits, _from, state) do
splits = Map.values(state.splits)
{:reply, splits, state}
end
@impl true
def handle_manager_call(
{:bind_buffer, split_id, buffer_pid, terminal_pid},
_from,
state
) do
case Map.get(state.splits, split_id) do
nil ->
{:reply, {:error, :not_found}, state}
split ->
updated = %{split | buffer_pid: buffer_pid, terminal_pid: terminal_pid}
new_state = %{state | splits: Map.put(state.splits, split_id, updated)}
{:reply, {:ok, updated}, new_state}
end
end
@impl true
def handle_manager_call({:unbind_buffer, split_id}, _from, state) do
case Map.get(state.splits, split_id) do
nil ->
{:reply, {:error, :not_found}, state}
split ->
updated = %{split | buffer_pid: nil, terminal_pid: nil}
new_state = %{state | splits: Map.put(state.splits, split_id, updated)}
{:reply, {:ok, updated}, new_state}
end
end
@impl true
def handle_manager_call({:get_split_buffer, split_id}, _from, state) do
case Map.get(state.splits, split_id) do
nil ->
{:reply, {:error, :not_found}, state}
%{buffer_pid: nil} ->
{:reply, {:error, :no_buffer}, state}
%{buffer_pid: pid} ->
{:reply, {:ok, pid}, state}
end
end
@impl true
def handle_manager_call({:set_label, split_id, label}, _from, state) do
case Map.get(state.splits, split_id) do
nil ->
{:reply, {:error, :not_found}, state}
split ->
updated = %{split | label: label}
new_state = %{state | splits: Map.put(state.splits, split_id, updated)}
{:reply, {:ok, updated}, new_state}
end
end
@impl true
def handle_manager_call({:remove_split, split_id}, _from, state) do
case Map.get(state.splits, split_id) do
nil ->
{:reply, {:error, :not_found}, state}
_split ->
new_state = %{state | splits: Map.delete(state.splits, split_id)}
{:reply, :ok, new_state}
end
end
end