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/cursor/movement.ex
defmodule Raxol.Terminal.Cursor.Movement do
@moduledoc """
Handles cursor movement and positioning for the terminal emulator.
This module provides functions for moving the cursor in various directions,
handling relative and absolute positioning, and managing cursor boundaries.
"""
alias Raxol.Terminal.Cursor.Manager
@doc """
Moves the cursor up by the specified number of lines, respecting margins.
## Examples
iex> alias Raxol.Terminal.Cursor.{Manager, Movement}
iex> cursor = Manager.new()
iex> cursor = Movement.move_up(cursor, 2)
iex> cursor.position
{0, 0} # Already at top, no change
"""
@spec move_up(
Cursor.t(),
non_neg_integer(),
non_neg_integer(),
non_neg_integer()
) :: Cursor.t()
def move_up(cursor, count \\ 1, _width, _height) do
# Move cursor up by count lines, but not above the top margin
new_y = max(cursor.y - count, cursor.top_margin)
Manager.move_to(cursor, cursor.x, new_y)
end
@doc """
Moves the cursor down by the specified number of lines, respecting margins.
## Examples
iex> alias Raxol.Terminal.Cursor.{Manager, Movement}
iex> cursor = Manager.new()
iex> cursor = Movement.move_down(cursor, 2)
iex> cursor.position
{0, 2}
"""
@spec move_down(
Cursor.t(),
non_neg_integer(),
non_neg_integer(),
non_neg_integer()
) :: Cursor.t()
def move_down(cursor, count \\ 1, _width, _height) do
# Move cursor down by count lines, but not below the bottom margin
new_y = min(cursor.y + count, cursor.bottom_margin)
Manager.move_to(cursor, cursor.x, new_y)
end
@doc """
Moves the cursor left by the specified number of columns.
## Examples
iex> alias Raxol.Terminal.Cursor.{Manager, Movement}
iex> cursor = Manager.new()
iex> cursor = Manager.move_to(cursor, 5, 0)
iex> cursor = Movement.move_left(cursor, 2)
iex> cursor.position
{3, 0}
"""
def move_left(cursor, count \\ 1) do
{x, y} = Manager.get_position(cursor)
new_x = max(0, x - count)
Manager.move_to(cursor, {new_x, y})
end
@doc """
Moves the cursor right by the specified number of columns.
## Examples
iex> alias Raxol.Terminal.Cursor.{Manager, Movement}
iex> cursor = Manager.new()
iex> cursor = Movement.move_right(cursor, 2)
iex> cursor.position
{2, 0}
"""
def move_right(cursor, count \\ 1) do
{x, y} = Manager.get_position(cursor)
new_x = x + count
Manager.move_to(cursor, {new_x, y})
end
@doc """
Moves the cursor to the beginning of the line.
## Examples
iex> alias Raxol.Terminal.Cursor.{Manager, Movement}
iex> cursor = Manager.new()
iex> cursor = Manager.move_to(cursor, 10, 0)
iex> cursor = Movement.move_to_line_start(cursor)
iex> cursor.position
{0, 0}
"""
def move_to_line_start(cursor) do
{_, y} = Manager.get_position(cursor)
Manager.move_to(cursor, {0, y})
end
@doc """
Moves the cursor to the end of the line.
## Examples
iex> alias Raxol.Terminal.Cursor.{Manager, Movement}
iex> cursor = Manager.new()
iex> cursor = Movement.move_to_line_end(cursor, 80)
iex> cursor.position
{79, 0}
"""
def move_to_line_end(cursor, line_width) do
{_, y} = Manager.get_position(cursor)
Manager.move_to(cursor, {line_width - 1, y})
end
@doc """
Moves the cursor to the specified column.
## Examples
iex> alias Raxol.Terminal.Cursor.{Manager, Movement}
iex> cursor = Manager.new()
iex> cursor = Movement.move_to_column(cursor, 10)
iex> cursor.position
{10, 0}
"""
def move_to_column(cursor, column) do
{_, y} = Manager.get_position(cursor)
Manager.move_to(cursor, {column, y})
end
@doc """
Moves the cursor to the specified line.
## Examples
iex> alias Raxol.Terminal.Cursor.{Manager, Movement}
iex> cursor = Manager.new()
iex> cursor = Movement.move_to_line(cursor, 5)
iex> cursor.position
{0, 5}
"""
def move_to_line(cursor, line) do
{x, _} = Manager.get_position(cursor)
Manager.move_to(cursor, {x, line})
end
@doc """
Moves the cursor to a specific position in the terminal.
"""
def move_to_position(cursor, row, col) do
Manager.move_to(cursor, row, col)
end
@doc """
Moves the cursor to a specific position with row bounds.
"""
def move_to(cursor, row, col, min_row, max_row) do
{_, current_col} = Manager.get_position(cursor)
new_row = max(min_row, min(max_row, row))
Manager.move_to(cursor, new_row, current_col)
end
@doc """
Moves the cursor to a specific position with both row and column bounds.
"""
def move_to(cursor, row, col, min_row, max_row, min_col, max_col) do
new_row = max(min_row, min(max_row, row))
new_col = max(min_col, min(max_col, col))
Manager.move_to(cursor, new_row, new_col)
end
@doc """
Moves the cursor to the home position (0, 0).
## Examples
iex> alias Raxol.Terminal.Cursor.{Manager, Movement}
iex> cursor = Manager.new()
iex> cursor = Manager.move_to(cursor, 10, 5)
iex> cursor = Movement.move_home(cursor)
iex> cursor.position
{0, 0}
"""
def move_home(cursor, width, height) do
Manager.move_to(cursor, 0, 0, width, height)
end
@doc """
Moves the cursor to the next tab stop.
## Examples
iex> alias Raxol.Terminal.Cursor.{Manager, Movement}
iex> cursor = Manager.new()
iex> cursor = Movement.move_to_next_tab(cursor, 8)
iex> cursor.position
{8, 0}
"""
def move_to_next_tab(cursor, tab_stops, width, height) do
{x, y} = Manager.get_position(cursor)
next_tab = find_next_tab(x, tab_stops, width)
Manager.move_to(cursor, next_tab, y, width, height)
end
@doc """
Moves the cursor to the previous tab stop.
## Examples
iex> alias Raxol.Terminal.Cursor.{Manager, Movement}
iex> cursor = Manager.new()
iex> cursor = Manager.move_to(cursor, 10, 0)
iex> cursor = Movement.move_to_prev_tab(cursor, 8)
iex> cursor.position
{8, 0}
"""
def move_to_prev_tab(cursor, tab_stops, width, height) do
{x, y} = Manager.get_position(cursor)
prev_tab = find_previous_tab(x, tab_stops)
Manager.move_to(cursor, prev_tab, y, width, height)
end
# Helper functions
defp find_next_tab(current_x, tab_stops, width) do
case Enum.find(tab_stops, fn stop -> stop > current_x end) do
nil -> width - 1
stop -> stop
end
end
defp find_previous_tab(current_x, tab_stops) do
case Enum.find(Enum.reverse(tab_stops), fn stop -> stop < current_x end) do
nil -> 0
stop -> stop
end
end
end