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