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/commands/csi_handler/cursor.ex
defmodule Raxol.Terminal.Commands.CSIHandler.Cursor do
@moduledoc """
Handles cursor-related CSI sequences.
"""
@doc """
Handles cursor movement commands.
"""
@spec handle_command(term(), list(), String.t()) ::
{:ok, term()} | {:error, term()}
def handle_command(emulator, params, command) do
# Delegate to the appropriate cursor movement handler
case command do
"A" -> handle_cursor_up(emulator, get_param(params, 0, 1))
"B" -> handle_cursor_down(emulator, get_param(params, 0, 1))
"C" -> handle_cursor_forward(emulator, get_param(params, 0, 1))
"D" -> handle_cursor_backward(emulator, get_param(params, 0, 1))
"H" -> handle_cursor_position(emulator, params)
"G" -> handle_cursor_column(emulator, get_param(params, 0, 1))
# Alternative position command
"f" -> handle_cursor_position(emulator, params)
"s" -> handle_save_cursor(emulator)
"u" -> handle_restore_cursor(emulator)
_ -> {:error, :unknown_cursor_command}
end
end
defp handle_cursor_up(emulator, amount) do
cursor = emulator.cursor
{current_row, current_col} = get_cursor_position(cursor)
new_row = max(0, current_row - amount)
updated_cursor = update_cursor_position(cursor, new_row, current_col)
{:ok, %{emulator | cursor: updated_cursor}}
end
defp handle_cursor_down(emulator, amount) do
cursor = emulator.cursor
{current_row, current_col} = get_cursor_position(cursor)
new_row = min(emulator.height - 1, current_row + amount)
updated_cursor = update_cursor_position(cursor, new_row, current_col)
{:ok, %{emulator | cursor: updated_cursor}}
end
defp handle_cursor_forward(emulator, amount) do
cursor = emulator.cursor
{current_row, current_col} = get_cursor_position(cursor)
new_col = min(emulator.width - 1, current_col + amount)
updated_cursor = update_cursor_position(cursor, current_row, new_col)
{:ok, %{emulator | cursor: updated_cursor}}
end
defp handle_cursor_backward(emulator, amount) do
cursor = emulator.cursor
{current_row, current_col} = get_cursor_position(cursor)
new_col = max(0, current_col - amount)
updated_cursor = update_cursor_position(cursor, current_row, new_col)
{:ok, %{emulator | cursor: updated_cursor}}
end
defp handle_cursor_position(emulator, params) do
# Convert from 1-based to 0-based
row = get_param(params, 0, 1) - 1
# Convert from 1-based to 0-based
col = get_param(params, 1, 1) - 1
# Clamp to valid ranges
bounded_row = max(0, min(emulator.height - 1, row))
bounded_col = max(0, min(emulator.width - 1, col))
cursor = emulator.cursor
# Handle both full cursor struct and minimal cursor (map with position tuple)
updated_cursor =
case cursor do
%{row: _, col: _} = c ->
# Full cursor struct
updated = %{c | row: bounded_row, col: bounded_col}
if Map.has_key?(c, :position) do
Map.put(updated, :position, {bounded_row, bounded_col})
else
updated
end
_ ->
# Minimal cursor - just update position
%{position: {bounded_row, bounded_col}}
end
{:ok, %{emulator | cursor: updated_cursor}}
end
defp handle_cursor_column(emulator, column) do
cursor = emulator.cursor
{current_row, _current_col} = get_cursor_position(cursor)
# Convert to 0-based
new_col = max(0, min(emulator.width - 1, column - 1))
updated_cursor = update_cursor_position(cursor, current_row, new_col)
{:ok, %{emulator | cursor: updated_cursor}}
end
defp handle_save_cursor(emulator) do
cursor = emulator.cursor
{current_row, current_col} = get_cursor_position(cursor)
updated_cursor =
case cursor do
%{row: _, col: _} = c ->
# Full cursor struct
%{
c
| saved_row: current_row,
saved_col: current_col,
saved_position: {current_row, current_col}
}
_ ->
# Minimal cursor
%{
position: {current_row, current_col},
saved_position: {current_row, current_col}
}
end
{:ok, %{emulator | cursor: updated_cursor}}
end
defp handle_restore_cursor(emulator) do
cursor = emulator.cursor
{new_row, new_col} =
case cursor do
%{saved_row: row, saved_col: col} when row != nil and col != nil ->
{row, col}
%{saved_position: {row, col}} ->
{row, col}
_ ->
# Default to origin if no saved position
{0, 0}
end
updated_cursor = update_cursor_position(cursor, new_row, new_col)
{:ok, %{emulator | cursor: updated_cursor}}
end
defp get_param(params, index, default) do
case Enum.at(params, index) do
nil -> default
val when is_integer(val) -> val
_ -> default
end
end
# Helper functions to handle both full and minimal cursor structs
defp get_cursor_position(cursor) do
case cursor do
%{row: row, col: col} when is_integer(row) and is_integer(col) ->
{row, col}
%{position: {row, col}} ->
{row, col}
_ ->
{0, 0}
end
end
defp update_cursor_position(cursor, new_row, new_col) do
case cursor do
%{row: _, col: _} = c ->
# Full cursor struct - update both row/col and position if it exists
c
|> Map.put(:row, new_row)
|> Map.put(:col, new_col)
|> then(fn cursor ->
if Map.has_key?(cursor, :position) do
Map.put(cursor, :position, {new_row, new_col})
else
cursor
end
end)
_ ->
# Minimal cursor - just update position
%{position: {new_row, new_col}}
end
end
end