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
raxol lib raxol terminal commands cursor_handler.ex
Raw

lib/raxol/terminal/commands/cursor_handler.ex

defmodule Raxol.Terminal.Commands.CursorHandler do
@moduledoc """
Handles cursor movement related CSI commands.
This module contains handlers for cursor movement commands like CUP, CUU, CUD, etc.
Each function takes the current emulator state and parsed parameters,
returning the updated emulator state.
"""
alias Raxol.Terminal.Emulator
alias Raxol.Terminal.Commands.CursorUtils
alias Raxol.Terminal.Cursor.Manager, as: CursorManager
require Raxol.Core.Runtime.Log
@spec handle_cursor_movement(
Emulator.t(),
atom(),
integer()
) :: {:ok, Emulator.t()} | {:error, atom(), Emulator.t()}
def handle_cursor_movement(emulator, direction, amount) do
cursor = emulator.cursor
{row, col} = get_cursor_position(cursor)
{new_row, new_col} =
CursorUtils.calculate_new_cursor_position(
{row, col},
direction,
amount,
emulator.width,
emulator.height
)
updated_cursor = set_cursor_position(cursor, {new_row, new_col})
{:ok, %{emulator | cursor: updated_cursor}}
end
@doc "Handles Cursor Position (CUP - \'H\")"
@spec handle_cup(Emulator.t(), list(integer())) ::
{:ok, Emulator.t()} | {:error, atom(), Emulator.t()}
def handle_cup(emulator, params) do
row = get_valid_pos_param(params, 0, 1)
col = get_valid_pos_param(params, 1, 1)
# Convert to 0-based coordinates
row_0 = row - 1
col_0 = col - 1
# Clamp to screen bounds
row_clamped = max(0, min(row_0, emulator.height - 1))
col_clamped = max(0, min(col_0, emulator.width - 1))
# Pass {row, col} to match new convention
updated_cursor =
set_cursor_position(emulator.cursor, {row_clamped, col_clamped})
{:ok, %{emulator | cursor: updated_cursor}}
end
@doc "Handles Cursor Position (CUP - 'H') - alias for handle_cup"
@spec handle_h_alias(Emulator.t(), list(integer())) ::
{:ok, Emulator.t()} | {:error, atom(), Emulator.t()}
def handle_h_alias(emulator, params) do
handle_cup(emulator, params)
end
@doc "Handles Cursor Up (CUU - \'A\')"
@spec handle_a_alias(Emulator.t(), list(integer())) ::
{:ok, Emulator.t()} | {:error, atom(), Emulator.t()}
def handle_a_alias(emulator, params) do
amount = Enum.at(params, 0, 1)
handle_cursor_movement(emulator, :up, amount)
end
@doc "Handles Cursor Down (CUD - \'B\') - alias for handle_B"
@spec handle_b_alias(Emulator.t(), list(integer())) ::
{:ok, Emulator.t()} | {:error, atom(), Emulator.t()}
def handle_b_alias(emulator, params) do
amount = Enum.at(params, 0, 1)
handle_cursor_movement(emulator, :down, amount)
end
@doc "Handles Cursor Forward (CUF - \'C\') - alias for handle_C"
@spec handle_c_alias(Emulator.t(), list(integer())) ::
{:ok, Emulator.t()} | {:error, atom(), Emulator.t()}
def handle_c_alias(emulator, params) do
amount = Enum.at(params, 0, 1)
handle_cursor_movement(emulator, :right, amount)
end
@doc "Handles Cursor Backward (CUB - \'D\') - alias for handle_D"
@spec handle_d_alias(Emulator.t(), list(integer())) ::
{:ok, Emulator.t()} | {:error, atom(), Emulator.t()}
def handle_d_alias(emulator, params) do
amount = Enum.at(params, 0, 1)
handle_cursor_movement(emulator, :left, amount)
end
@doc """
Handles Cursor Next Line (CNL - 'E').
"""
@spec handle_e_alias(Emulator.t(), list(integer())) ::
{:ok, Emulator.t()} | {:error, atom(), Emulator.t()}
def handle_e_alias(emulator, params) do
amount = Enum.at(params, 0, 1)
cursor = emulator.cursor
{row, _col} = get_cursor_position(cursor)
# Move down by amount, clamp to screen height
new_row = min(emulator.height - 1, row + amount)
# Move to beginning of line (column 0) at the new row
updated_cursor = set_cursor_position(cursor, {new_row, 0})
{:ok, %{emulator | cursor: updated_cursor}}
end
@doc """
Handles Cursor Previous Line (CPL - 'F').
"""
@spec handle_f(Emulator.t(), list(integer())) ::
{:ok, Emulator.t()} | {:error, atom(), Emulator.t()}
def handle_f(emulator, params) do
amount = Enum.at(params, 0, 1)
cursor = emulator.cursor
{row, _col} = get_cursor_position(cursor)
# Move up by amount, clamp to screen top
new_row = max(0, row - amount)
# Move to beginning of line (column 0) at the new row
updated_cursor = set_cursor_position(cursor, {new_row, 0})
{:ok, %{emulator | cursor: updated_cursor}}
end
@doc """
Handles Cursor Previous Line (CPL - 'F') - alias for handle_f.
"""
@spec handle_f_alias(Emulator.t(), list(integer())) ::
{:ok, Emulator.t()} | {:error, atom(), Emulator.t()}
def handle_f_alias(emulator, params) do
handle_f(emulator, params)
end
@doc """
Handles Cursor Horizontal Absolute (CHA - 'G').
"""
@spec handle_g(Emulator.t(), list(integer())) ::
{:ok, Emulator.t()} | {:error, atom(), Emulator.t()}
def handle_g(emulator, params) do
# handle_g is the same as handle_G (Cursor Horizontal Absolute)
handle_G(emulator, params)
end
@doc """
Handles Cursor Horizontal Absolute (CHA - 'G') - alias for handle_g.
"""
@spec handle_g_alias(Emulator.t(), list(integer())) ::
{:ok, Emulator.t()} | {:error, atom(), Emulator.t()}
def handle_g_alias(emulator, params) do
# DEBUG: handle_G called with params=#{inspect(params)}, cursor=#{inspect(emulator.cursor)}
column = Enum.at(params, 0, 1)
# Convert to 0-based
column_0 = column - 1
# Clamp to screen width
column_clamped = max(0, min(column_0, emulator.width - 1))
cursor = emulator.cursor
{current_row, _} = get_cursor_position(cursor)
updated_cursor = set_cursor_position(cursor, {current_row, column_clamped})
{:ok, %{emulator | cursor: updated_cursor}}
end
@doc "Handles Cursor Vertical Absolute (VPA - \'d\') - alias for handle_decvpa"
@spec handle_d(Emulator.t(), list(integer())) ::
{:ok, Emulator.t()} | {:error, atom(), Emulator.t()}
def handle_d(emulator, params) do
handle_decvpa(emulator, params)
end
@doc "Handles Cursor Vertical Absolute (VPA - \'d\")"
@spec handle_decvpa(Emulator.t(), list(integer())) ::
{:ok, Emulator.t()} | {:error, atom(), Emulator.t()}
def handle_decvpa(emulator, params) do
row = get_valid_pos_param(params, 0, 1)
# Convert to 0-based
row_0 = row - 1
# Clamp to screen height
row_clamped = max(0, min(row_0, emulator.height - 1))
cursor = emulator.cursor
{_, current_col} = get_cursor_position(cursor)
updated_cursor = set_cursor_position(cursor, {row_clamped, current_col})
{:ok, %{emulator | cursor: updated_cursor}}
end
@doc """
Moves the cursor to a specific position with width and height bounds.
"""
@spec move_cursor_to(
Emulator.t(),
{integer(), integer()},
integer(),
integer()
) :: Emulator.t()
def move_cursor_to(emulator, position, width, height) do
{row, col} = position
# Clamp coordinates to screen bounds
row_clamped = max(0, min(row, height - 1))
col_clamped = max(0, min(col, width - 1))
updated_cursor =
set_cursor_position(emulator.cursor, {row_clamped, col_clamped})
%{emulator | cursor: updated_cursor}
end
@doc """
Moves the cursor to a specific position.
"""
@spec move_cursor_to(Emulator.t(), integer(), integer()) :: Emulator.t()
def move_cursor_to(emulator, x, y) do
move_cursor_to(emulator, {x, y}, emulator.width, emulator.height)
end
@doc """
Moves the cursor up by the specified number of lines.
"""
@spec move_cursor_up(Emulator.t(), non_neg_integer()) :: Emulator.t()
def move_cursor_up(emulator, count \\ 1) do
cursor = emulator.cursor
{row, col} = get_cursor_position(cursor)
new_row = max(0, row - count)
updated_cursor = set_cursor_position(cursor, {new_row, col})
%{emulator | cursor: updated_cursor}
end
@doc """
Moves the cursor up by the specified number of lines with width and height bounds.
"""
@spec move_cursor_up(
Emulator.t(),
non_neg_integer(),
non_neg_integer(),
non_neg_integer()
) :: Emulator.t()
def move_cursor_up(emulator, count, _width, _height) do
cursor = emulator.cursor
{row, col} = get_cursor_position(cursor)
new_row = max(0, row - count)
updated_cursor = set_cursor_position(cursor, {new_row, col})
%{emulator | cursor: updated_cursor}
end
@doc """
Moves the cursor down by the specified number of lines.
"""
@spec move_cursor_down(Emulator.t(), non_neg_integer()) :: Emulator.t()
def move_cursor_down(emulator, count \\ 1) do
cursor = emulator.cursor
{row, col} = get_cursor_position(cursor)
new_row = min(emulator.height - 1, row + count)
updated_cursor = set_cursor_position(cursor, {new_row, col})
%{emulator | cursor: updated_cursor}
end
@doc """
Moves the cursor down by the specified number of lines with width and height bounds.
"""
@spec move_cursor_down(
Emulator.t(),
non_neg_integer(),
non_neg_integer(),
non_neg_integer()
) :: Emulator.t()
def move_cursor_down(emulator, count, _width, height) do
cursor = emulator.cursor
{row, col} = get_cursor_position(cursor)
new_row = min(height - 1, row + count)
updated_cursor = set_cursor_position(cursor, {new_row, col})
%{emulator | cursor: updated_cursor}
end
@doc """
Moves the cursor left by the specified number of columns.
"""
@spec move_cursor_left(
Emulator.t(),
non_neg_integer(),
non_neg_integer(),
non_neg_integer()
) :: Emulator.t()
def move_cursor_left(emulator, count, _width, _height) do
cursor = emulator.cursor
{row, col} = get_cursor_position(cursor)
new_col = max(0, col - count)
updated_cursor = set_cursor_position(cursor, {row, new_col})
%{emulator | cursor: updated_cursor}
end
@doc """
Moves the cursor right by the specified number of columns.
"""
@spec move_cursor_right(
Emulator.t(),
non_neg_integer(),
non_neg_integer(),
non_neg_integer()
) :: Emulator.t()
def move_cursor_right(emulator, count, width, _height) do
cursor = emulator.cursor
{row, col} = get_cursor_position(cursor)
new_col = min(width - 1, col + count)
updated_cursor = set_cursor_position(cursor, {row, new_col})
%{emulator | cursor: updated_cursor}
end
@doc """
Moves the cursor forward by the specified number of columns.
"""
@spec move_cursor_forward(Emulator.t(), non_neg_integer()) :: Emulator.t()
def move_cursor_forward(emulator, count) do
move_cursor_right(emulator, count, emulator.width, emulator.height)
end
@doc """
Moves the cursor back by the specified number of columns.
"""
@spec move_cursor_back(Emulator.t(), non_neg_integer()) :: Emulator.t()
def move_cursor_back(emulator, count) do
move_cursor_left(emulator, count, emulator.width, emulator.height)
end
@doc """
Moves the cursor to the start of the current line.
"""
@spec move_cursor_to_line_start(Emulator.t()) :: Emulator.t()
def move_cursor_to_line_start(emulator) do
cursor = emulator.cursor
{row, _col} = get_cursor_position(cursor)
updated_cursor = set_cursor_position(cursor, {row, 0})
%{emulator | cursor: updated_cursor}
end
@doc """
Moves the cursor to a specific column.
"""
@spec move_cursor_to_column(
Emulator.t(),
non_neg_integer(),
non_neg_integer(),
non_neg_integer()
) :: Emulator.t()
def move_cursor_to_column(emulator, column, width, _height) do
cursor = emulator.cursor
{row, _col} = get_cursor_position(cursor)
new_col = max(0, min(width - 1, column))
updated_cursor = set_cursor_position(cursor, {row, new_col})
%{emulator | cursor: updated_cursor}
end
defp get_valid_pos_param(params, index, default) do
case Enum.at(params, index) do
nil -> default
value when is_integer(value) and value > 0 -> value
_ -> default
end
end
# Helper functions to handle both cursor structs and PIDs
defp set_cursor_position(nil, {row, col}) do
%{position: {row, col}, row: row, col: col}
end
defp set_cursor_position(%CursorManager{} = cursor, {row, col}) do
%CursorManager{cursor | row: row, col: col, position: {row, col}}
end
defp set_cursor_position(cursor, position) when is_pid(cursor) do
CursorManager.set_position(cursor, position)
end
defp set_cursor_position(cursor, position) when is_map(cursor) do
{row, col} = position
# Check if cursor has both row and col fields
case {Map.has_key?(cursor, :row), Map.has_key?(cursor, :col)} do
{true, true} ->
# Also update position field if it exists
case Map.has_key?(cursor, :position) do
true -> %{cursor | row: row, col: col, position: {row, col}}
false -> %{cursor | row: row, col: col}
end
_ ->
# If cursor doesn't have expected fields, just return it unchanged
cursor
end
end
# Fallback clause for any other cursor type
defp set_cursor_position(cursor, {row, col}) do
# Try to handle any cursor type that might have row and col fields
case cursor do
%{row: _, col: _} when is_map(cursor) ->
%{cursor | row: row, col: col}
_ ->
# If we can't handle it, return the cursor unchanged
cursor
end
end
defp get_cursor_position(nil) do
{0, 0}
end
defp get_cursor_position(cursor) when is_pid(cursor) do
CursorManager.get_position(cursor)
end
defp get_cursor_position(cursor) when is_map(cursor) do
cursor.position
end
@doc """
Handles CSI A - Cursor Up (CUU)
"""
def handle_A(emulator, params) do
count = get_valid_pos_param(params, 0, 1)
{:ok, move_cursor_up(emulator, count)}
end
@doc """
Handles CSI B - Cursor Down (CUD)
"""
def handle_B(emulator, params) do
count = get_valid_pos_param(params, 0, 1)
{:ok, move_cursor_down(emulator, count)}
end
@doc """
Handles CSI C - Cursor Forward (CUF)
"""
def handle_C(emulator, params) do
count = get_valid_pos_param(params, 0, 1)
{:ok, move_cursor_forward(emulator, count)}
end
@doc """
Handles CSI D - Cursor Back (CUB)
"""
def handle_D(emulator, params) do
count = get_valid_pos_param(params, 0, 1)
{:ok, move_cursor_back(emulator, count)}
end
@doc """
Handles CSI G - Cursor Horizontal Absolute (CHA)
"""
def handle_G(emulator, params) do
# Convert to 0-based
column = get_valid_pos_param(params, 0, 1) - 1
{:ok,
move_cursor_to_column(emulator, column, emulator.width, emulator.height)}
end
@doc """
Handles CSI E - Cursor Next Line (CNL)
Moves cursor to beginning of line n lines down
"""
def handle_E(emulator, params) do
count = get_valid_pos_param(params, 0, 1)
# Move down by count lines
emulator_after_down = move_cursor_down(emulator, count)
# Move to column 0
{:ok,
move_cursor_to_column(
emulator_after_down,
0,
emulator.width,
emulator.height
)}
end
@doc """
Handles CSI F - Cursor Previous Line (CPL)
Moves cursor to beginning of line n lines up
"""
def handle_F(emulator, params) do
count = get_valid_pos_param(params, 0, 1)
# Move up by count lines
emulator_after_up = move_cursor_up(emulator, count)
# Move to column 0
{:ok,
move_cursor_to_column(
emulator_after_up,
0,
emulator.width,
emulator.height
)}
end
@doc """
Handles CSI H - Cursor Position (CUP)
Sets cursor position to row;column
"""
def handle_H(emulator, params) do
handle_cup(emulator, params)
end
end