Packages
Multi-surface application runtime for Elixir. One TEA module renders to terminal, browser (LiveView), SSH, and MCP (agents). 30+ widgets, flexbox + CSS grid, AI agent runtime, distributed swarm with CRDTs, time-travel debugging, session recording, sandboxed REPL, and agentic commerce.
Current section
Files
Jump to
Current section
Files
lib/raxol/terminal/buffer/cursor.ex
defmodule Raxol.Terminal.Buffer.Cursor do
@moduledoc """
Manages cursor state and operations for the screen buffer.
This module handles cursor position, visibility, style, and blink state.
"""
alias Raxol.Terminal.ScreenBuffer
@type cursor_style :: :block | :underline | :bar
@type t :: %__MODULE__{
position: {non_neg_integer(), non_neg_integer()},
visible: boolean(),
style: cursor_style(),
blink_state: boolean()
}
defstruct [
:position,
:visible,
:style,
:blink_state
]
@doc """
Initializes a new cursor state with default values.
"""
@spec init() :: t()
def init do
%__MODULE__{
position: {0, 0},
visible: true,
style: :block,
blink_state: true
}
end
@doc """
Sets the cursor position.
## Parameters
* `buffer` - The screen buffer to modify
* `x` - The x-coordinate (column)
* `y` - The y-coordinate (row)
## Returns
The updated screen buffer with new cursor position.
## Examples
iex> buffer = ScreenBuffer.new(80, 24)
iex> buffer = Cursor.set_position(buffer, 10, 5)
iex> Cursor.get_position(buffer)
{10, 5}
"""
@spec set_position(map(), non_neg_integer(), non_neg_integer()) :: map()
def set_position(buffer, x, y) do
case buffer do
%{cursor: cursor} ->
new_cursor = %{cursor | position: {x, y}}
%{buffer | cursor: new_cursor}
%{cursor_position: _} ->
%{buffer | cursor_position: {x, y}}
_ ->
buffer
end
end
@doc """
Gets the current cursor position.
## Parameters
* `buffer` - The screen buffer to query
## Returns
A tuple {x, y} representing the cursor position.
## Examples
iex> buffer = ScreenBuffer.new(80, 24)
iex> Cursor.get_position(buffer)
{0, 0}
"""
@spec get_position(ScreenBuffer.t()) :: {non_neg_integer(), non_neg_integer()}
def get_position(buffer) do
buffer.cursor.position
end
@doc """
Sets the cursor visibility.
## Parameters
* `buffer` - The screen buffer to modify
* `visible` - Whether the cursor should be visible
## Returns
The updated screen buffer with new cursor visibility.
## Examples
iex> buffer = ScreenBuffer.new(80, 24)
iex> buffer = Cursor.set_visibility(buffer, false)
iex> Cursor.visible?(buffer)
false
"""
@spec set_visibility(ScreenBuffer.t(), boolean()) :: ScreenBuffer.t()
def set_visibility(buffer, visible) do
%{buffer | cursor_visible: visible}
end
@doc """
Checks if the cursor is visible.
## Parameters
* `buffer` - The screen buffer to query
## Returns
A boolean indicating cursor visibility.
## Examples
iex> buffer = ScreenBuffer.new(80, 24)
iex> Cursor.visible?(buffer)
true
"""
@spec visible?(ScreenBuffer.t() | t()) :: boolean()
def visible?(%ScreenBuffer{} = buffer) do
buffer.cursor_visible
end
def visible?(%__MODULE__{} = cursor) do
cursor.visible
end
@doc """
Sets the cursor style.
## Parameters
* `buffer` - The screen buffer to modify
* `style` - The cursor style (:block, :underline, :bar)
## Returns
The updated screen buffer with new cursor style.
## Examples
iex> buffer = ScreenBuffer.new(80, 24)
iex> buffer = Cursor.set_style(buffer, :underline)
iex> Cursor.get_style(buffer)
:underline
"""
@spec set_style(ScreenBuffer.t(), atom()) :: ScreenBuffer.t()
def set_style(buffer, style) do
Map.put(buffer, :cursor_style, style)
end
@doc """
Gets the current cursor style.
## Parameters
* `buffer` - The screen buffer to query
## Returns
The current cursor style.
## Examples
iex> buffer = ScreenBuffer.new(80, 24)
iex> Cursor.get_style(buffer)
:block
"""
@spec get_style(ScreenBuffer.t()) :: atom()
def get_style(buffer) do
buffer.cursor.style
end
@doc """
Sets the cursor blink state.
## Parameters
* `buffer` - The screen buffer to modify
* `blink` - Whether the cursor should blink
## Returns
The updated screen buffer with new cursor blink state.
## Examples
iex> buffer = ScreenBuffer.new(80, 24)
iex> buffer = Cursor.set_blink(buffer, false)
iex> Cursor.blinking?(buffer)
false
"""
@spec set_blink(ScreenBuffer.t(), boolean()) :: ScreenBuffer.t()
def set_blink(buffer, blink) do
Map.put(buffer, :cursor_blink, blink)
end
@doc """
Checks if the cursor is blinking.
## Parameters
* `buffer` - The screen buffer to query
## Returns
A boolean indicating if the cursor is blinking.
## Examples
iex> buffer = ScreenBuffer.new(80, 24)
iex> Cursor.blinking?(buffer)
true
"""
@spec blinking?(ScreenBuffer.t() | t()) :: boolean()
def blinking?(%ScreenBuffer{} = _buffer) do
# ScreenBuffer doesn't have cursor blink state, default to false
false
end
def blinking?(%__MODULE__{} = cursor) do
cursor.blink_state
end
@doc """
Moves the cursor up by the specified number of lines.
## Parameters
* `buffer` - The screen buffer to modify
* `lines` - Number of lines to move up
## Returns
The updated screen buffer with new cursor position.
## Examples
iex> buffer = ScreenBuffer.new(80, 24)
iex> buffer = Cursor.move_up(buffer, 2)
iex> Cursor.get_position(buffer)
{0, 0} # Cursor stays at top
"""
@spec move_up(ScreenBuffer.t(), non_neg_integer()) :: ScreenBuffer.t()
def move_up(buffer, lines) do
{x, y} = buffer.cursor.position
new_y = max(0, y - lines)
set_position(buffer, x, new_y)
end
@doc """
Moves the cursor down by the specified number of lines.
## Parameters
* `buffer` - The screen buffer to modify
* `lines` - Number of lines to move down
## Returns
The updated screen buffer with new cursor position.
## Examples
iex> buffer = ScreenBuffer.new(80, 24)
iex> buffer = Cursor.move_down(buffer, 2)
iex> Cursor.get_position(buffer)
{0, 2}
"""
@spec move_down(ScreenBuffer.t(), non_neg_integer()) :: ScreenBuffer.t()
def move_down(buffer, lines) do
{x, y} = buffer.cursor.position
new_y = min(buffer.height - 1, y + lines)
set_position(buffer, x, new_y)
end
@doc """
Moves the cursor forward by the specified number of columns.
## Parameters
* `buffer` - The screen buffer to modify
* `columns` - Number of columns to move forward
## Returns
The updated screen buffer with new cursor position.
## Examples
iex> buffer = ScreenBuffer.new(80, 24)
iex> buffer = Cursor.move_forward(buffer, 2)
iex> Cursor.get_position(buffer)
{2, 0}
"""
@spec move_forward(ScreenBuffer.t(), non_neg_integer()) :: ScreenBuffer.t()
def move_forward(buffer, columns) do
{x, y} = buffer.cursor.position
new_x = min(buffer.width - 1, x + columns)
set_position(buffer, new_x, y)
end
@doc """
Moves the cursor backward by the specified number of columns.
## Parameters
* `buffer` - The screen buffer to modify
* `columns` - Number of columns to move backward
## Returns
The updated screen buffer with new cursor position.
## Examples
iex> buffer = ScreenBuffer.new(80, 24)
iex> buffer = Cursor.move_backward(buffer, 2)
iex> Cursor.get_position(buffer)
{0, 0} # Cursor stays at left edge
"""
@spec move_backward(ScreenBuffer.t(), non_neg_integer()) :: ScreenBuffer.t()
def move_backward(buffer, columns) do
{x, y} = buffer.cursor.position
new_x = max(0, x - columns)
set_position(buffer, new_x, y)
end
@doc """
Moves the cursor to the specified position.
## Parameters
* `buffer` - The screen buffer to modify
* `position` - The target position as {x, y}
## Returns
The updated screen buffer with new cursor position.
## Examples
iex> buffer = ScreenBuffer.new(80, 24)
iex> buffer = Cursor.move_to(buffer, {10, 5})
iex> Cursor.get_position(buffer)
{10, 5}
"""
@spec move_to(ScreenBuffer.t(), {non_neg_integer(), non_neg_integer()}) ::
ScreenBuffer.t()
def move_to(buffer, {x, y}) do
x = max(0, min(buffer.width - 1, x))
y = max(0, min(buffer.height - 1, y))
set_position(buffer, x, y)
end
@doc """
Sets the cursor position on the ScreenBuffer struct.
"""
@spec set_cursor_position(
ScreenBuffer.t(),
non_neg_integer(),
non_neg_integer()
) :: ScreenBuffer.t()
def set_cursor_position(buffer, x, y) do
%{buffer | cursor_position: {x, y}}
end
@doc """
Gets the cursor position from the ScreenBuffer struct.
"""
@spec get_cursor_position(ScreenBuffer.t()) ::
{non_neg_integer(), non_neg_integer()} | nil
def get_cursor_position(buffer) do
buffer.cursor_position
end
end