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/selection.ex
defmodule Raxol.Terminal.Buffer.Selection do
@moduledoc """
Manages text selection operations for the terminal.
This module handles all selection-related operations including:
- Starting and updating selections
- Getting selected text
- Checking if positions are within selections
- Managing selection boundaries
- Extracting text from regions
"""
alias Raxol.Terminal.ScreenBuffer
@doc """
Creates a new selection with start and end positions.
"""
@spec new(
{non_neg_integer(), non_neg_integer()},
{non_neg_integer(), non_neg_integer()}
) ::
{non_neg_integer(), non_neg_integer(), non_neg_integer(), non_neg_integer()}
def new({start_x, start_y}, {end_x, end_y}) do
{start_x, start_y, end_x, end_y}
end
@doc """
Starts a text selection at the specified position.
"""
@spec start(ScreenBuffer.t(), non_neg_integer(), non_neg_integer()) ::
ScreenBuffer.t()
def start(buffer, x, y) do
# Clear any existing selection first
buffer = clear(buffer)
%{buffer | selection: {x, y, x, y}}
end
@doc """
Updates the current text selection to the specified position.
"""
@spec update(ScreenBuffer.t(), non_neg_integer(), non_neg_integer()) ::
ScreenBuffer.t()
def update(buffer, x, y) do
case buffer.selection do
{start_x, start_y, _, _} ->
%{buffer | selection: {start_x, start_y, x, y}}
nil ->
start(buffer, x, y)
end
end
@doc """
Gets the currently selected text.
"""
@spec get_text(ScreenBuffer.t()) :: String.t()
def get_text(buffer) do
case buffer.selection do
nil ->
""
{start_x, start_y, end_x, end_y} ->
get_text_in_region(buffer, start_x, start_y, end_x, end_y)
end
end
@doc """
Checks if a position is within the current selection.
"""
@spec contains?(ScreenBuffer.t(), non_neg_integer(), non_neg_integer()) ::
boolean()
def contains?(buffer, x, y) do
case buffer.selection do
nil ->
false
{start_x, start_y, end_x, end_y} ->
# Normalize coordinates to ensure start <= end
{min_x, max_x} = {min(start_x, end_x), max(start_x, end_x)}
{min_y, max_y} = {min(start_y, end_y), max(start_y, end_y)}
x >= min_x and x <= max_x and y >= min_y and y <= max_y
end
end
@doc """
Gets the current selection boundaries.
"""
@spec get_boundaries(ScreenBuffer.t()) ::
{non_neg_integer(), non_neg_integer(), non_neg_integer(), non_neg_integer()}
| nil
def get_boundaries(buffer) do
case buffer.selection do
nil ->
nil
{start_x, start_y, end_x, end_y} ->
# Normalize coordinates to ensure start <= end
{min_x, max_x} = {min(start_x, end_x), max(start_x, end_x)}
{min_y, max_y} = {min(start_y, end_y), max(start_y, end_y)}
{min_x, min_y, max_x, max_y}
end
end
@doc """
Gets text from a specified region in the buffer.
"""
@spec get_text_in_region(
ScreenBuffer.t(),
non_neg_integer(),
non_neg_integer(),
non_neg_integer(),
non_neg_integer()
) :: String.t()
def get_text_in_region(buffer, start_x, start_y, end_x, end_y) do
# Check if coordinates are out of bounds
case out_of_bounds?(buffer, start_x, start_y, end_x, end_y) do
true ->
""
false ->
# Ensure start coordinates are less than or equal to end coordinates
{start_x, end_x} = {min(start_x, end_x), max(start_x, end_x)}
{start_y, end_y} = {min(start_y, end_y), max(start_y, end_y)}
# Handle empty region (same start and end coordinates)
handle_region_extraction(buffer, start_x, start_y, end_x, end_y)
end
end
defp handle_region_extraction(_buffer, start_x, start_y, end_x, end_y)
when start_x == end_x and start_y == end_y do
""
end
defp handle_region_extraction(buffer, start_x, start_y, end_x, end_y) do
extract_region_text(buffer, start_x, start_y, end_x, end_y)
end
defp out_of_bounds?(buffer, start_x, start_y, end_x, end_y) do
start_x >= buffer.width or end_x >= buffer.width or
start_y >= buffer.height or end_y >= buffer.height or
start_x < 0 or start_y < 0 or end_x < 0 or end_y < 0
end
defp extract_region_text(buffer, start_x, start_y, end_x, end_y) do
case buffer.cells do
nil ->
# Return empty string if cells is nil
""
cells ->
text =
for y <- start_y..end_y do
line = Enum.at(cells, y) || []
chars =
for x <- start_x..end_x do
cell = Enum.at(line, x)
get_cell_char(cell)
end
Enum.join(chars)
end
Enum.join(text, "\n")
end
end
@doc """
Clears the current selection.
"""
@spec clear(ScreenBuffer.t()) :: ScreenBuffer.t()
def clear(buffer) do
%{buffer | selection: nil}
end
@doc """
Checks if there is an active selection.
"""
@spec active?(ScreenBuffer.t()) :: boolean()
def active?(buffer) do
buffer.selection != nil
end
@doc """
Gets the selection start position.
"""
@spec get_start_position(ScreenBuffer.t()) ::
{non_neg_integer(), non_neg_integer()} | nil
def get_start_position(buffer) do
case buffer.selection do
{start_x, start_y, _, _} -> {start_x, start_y}
nil -> nil
end
end
@doc """
Gets the selection end position.
"""
@spec get_end_position(ScreenBuffer.t()) ::
{non_neg_integer(), non_neg_integer()} | nil
def get_end_position(buffer) do
case buffer.selection do
{start_x, start_y, end_x, end_y} ->
get_position_if_different(start_x, start_y, end_x, end_y)
nil ->
nil
end
end
defp get_position_if_different(start_x, start_y, end_x, end_y)
when start_x == end_x and start_y == end_y do
nil
end
defp get_position_if_different(_start_x, _start_y, end_x, end_y) do
{end_x, end_y}
end
@doc """
Gets a line from a list of strings at the specified index.
"""
@spec get_line(list(String.t()), non_neg_integer()) :: String.t()
def get_line(lines_list, row) when is_list(lines_list) and is_integer(row) do
Enum.at(lines_list, row, "")
end
@doc """
Gets the buffer text for the current selection.
This is an alias for get_text/1 for compatibility.
"""
@spec get_buffer_text(ScreenBuffer.t()) ::
{:ok, String.t()} | {:error, term()}
def get_buffer_text(buffer) do
case Raxol.Core.ErrorHandling.safe_call(fn -> get_text(buffer) end) do
{:ok, text} -> {:ok, text}
{:error, e} -> {:error, e}
end
end
defp get_cell_char(nil), do: " "
defp get_cell_char(cell), do: cell.char
end