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
raxol_terminal lib raxol terminal selection selection_manager.ex
Raw

lib/raxol/terminal/selection/selection_manager.ex

defmodule Raxol.Terminal.Selection.Manager do
@moduledoc """
Manages text selection operations in the terminal.
"""
defstruct start_pos: nil,
end_pos: nil,
active: false,
# :normal, :word, :line
mode: :normal,
scrollback_included: false
@type position :: {non_neg_integer(), non_neg_integer()}
@type selection_mode :: :normal | :word | :line
@type t :: %__MODULE__{
start_pos: position() | nil,
end_pos: position() | nil,
active: boolean(),
mode: selection_mode(),
scrollback_included: boolean()
}
@doc """
Creates a new selection manager instance.
"""
def new do
%__MODULE__{}
end
@doc """
Starts a new selection at the given position.
"""
def start_selection(%__MODULE__{} = state, pos, mode \\ :normal)
when is_tuple(pos) and tuple_size(pos) == 2 and
mode in [:normal, :word, :line] do
%{state | start_pos: pos, end_pos: pos, active: true, mode: mode}
end
@doc """
Updates the selection end position.
"""
def update_selection(%__MODULE__{} = state, pos)
when is_tuple(pos) and tuple_size(pos) == 2 do
update_selection_if_active(state.active, state, pos)
end
@doc """
Ends the current selection.
"""
def end_selection(%__MODULE__{} = state) do
%{state | active: false, start_pos: nil, end_pos: nil}
end
@doc """
Gets the current selection range.
"""
def get_selection_range(%__MODULE__{} = state) do
get_range_if_valid(
state.active and state.start_pos and state.end_pos,
state
)
end
@doc """
Checks if a position is within the current selection.
"""
def position_in_selection?(%__MODULE__{} = state, pos)
when is_tuple(pos) and tuple_size(pos) == 2 do
check_position_in_selection(
state.active and state.start_pos and state.end_pos,
state,
pos
)
end
@doc """
Gets the selected text from the terminal buffer.
"""
def get_selected_text(%__MODULE__{} = state, buffer) do
get_text_if_valid_selection(
state.active and state.start_pos and state.end_pos,
state,
buffer
)
end
@doc """
Includes scrollback buffer in selection.
"""
def include_scrollback(%__MODULE__{} = state, include \\ true) do
%{state | scrollback_included: include}
end
@doc """
Checks if scrollback is included in selection.
"""
def scrollback_included?(%__MODULE__{} = state) do
state.scrollback_included
end
# Private helper functions
defp update_selection_if_active(false, state, _pos), do: state
defp update_selection_if_active(true, state, pos), do: %{state | end_pos: pos}
defp get_range_if_valid(false, _state), do: nil
defp get_range_if_valid(true, state), do: {state.start_pos, state.end_pos}
defp check_position_in_selection(false, _state, _pos), do: false
defp check_position_in_selection(true, state, pos) do
{start_x, start_y} = state.start_pos
{end_x, end_y} = state.end_pos
{x, y} = pos
check_line_type_selection(
start_y == end_y,
y,
x,
start_y,
end_y,
start_x,
end_x
)
end
defp check_line_type_selection(true, y, x, start_y, _end_y, start_x, end_x) do
# Single line selection
y == start_y and x >= min(start_x, end_x) and x <= max(start_x, end_x)
end
defp check_line_type_selection(false, y, x, start_y, end_y, start_x, end_x) do
# Multi-line selection
check_multiline_position(y, x, start_y, end_y, start_x, end_x)
end
defp get_text_if_valid_selection(false, _state, _buffer), do: ""
defp get_text_if_valid_selection(true, state, buffer) do
{start_x, start_y} = state.start_pos
{end_x, end_y} = state.end_pos
get_text_by_line_type(
start_y == end_y,
buffer,
start_y,
end_y,
start_x,
end_x
)
end
defp get_text_by_line_type(true, buffer, start_y, _end_y, start_x, end_x) do
get_line_selection(buffer, start_y, start_x, end_x)
end
defp get_text_by_line_type(false, buffer, start_y, end_y, start_x, end_x) do
get_multiline_selection(buffer, start_y, end_y, start_x, end_x)
end
defp get_line_selection(buffer, y, start_x, end_x) do
case Enum.at(buffer, y) do
nil ->
""
line ->
start_idx = min(start_x, end_x)
end_idx = max(start_x, end_x)
String.slice(line, start_idx, end_idx - start_idx + 1)
end
end
defp get_multiline_selection(buffer, start_y, end_y, start_x, end_x) do
buffer
|> Enum.slice(start_y..end_y)
|> Enum.with_index()
|> Enum.map_join("\n", fn {line, idx} ->
slice_line_by_index(line, idx, end_y - start_y, start_x, end_x)
end)
end
defp check_multiline_position(y, x, start_y, _end_y, start_x, _end_x)
when y == start_y,
do: x >= start_x
defp check_multiline_position(y, x, _start_y, end_y, _start_x, end_x)
when y == end_y,
do: x <= end_x
defp check_multiline_position(y, _x, start_y, end_y, _start_x, _end_x)
when y > start_y and y < end_y,
do: true
defp check_multiline_position(_y, _x, _start_y, _end_y, _start_x, _end_x),
do: false
defp slice_line_by_index(line, 0, _last_index, start_x, _end_x) do
String.slice(line, start_x..-1)
end
defp slice_line_by_index(line, idx, last_index, _start_x, end_x)
when idx == last_index do
String.slice(line, 0..end_x)
end
defp slice_line_by_index(line, _idx, _last_index, _start_x, _end_x), do: line
end