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/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