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/emulator/command_handler.ex
defmodule Raxol.Terminal.Emulator.CommandHandler do
@moduledoc """
Handles CSI/ESC/SGR/OSC and related command logic for the terminal emulator.
Extracted from the main emulator module for clarity and maintainability.
"""
# Import/alias as needed for dependencies
alias Raxol.Terminal.Emulator
# handle_cursor_position/2
def handle_cursor_position(params, emulator) do
case String.split(params, ";") do
[row_str, col_str] ->
row = String.to_integer(row_str)
col = String.to_integer(col_str)
Raxol.Terminal.Commands.CursorHandler.move_cursor_to(
emulator,
{row - 1, col - 1},
emulator.width,
emulator.height
)
[pos_str] ->
pos = String.to_integer(pos_str)
Raxol.Terminal.Commands.CursorHandler.move_cursor_to(
emulator,
{0, pos - 1},
emulator.width,
emulator.height
)
_ ->
emulator
end
end
# handle_cursor_up/2
def handle_cursor_up(params, emulator) do
count =
case params do
"" -> 1
count_str -> String.to_integer(count_str)
end
Emulator.move_cursor_up(emulator, count)
end
# handle_cursor_down/2
def handle_cursor_down(params, emulator) do
count =
case params do
"" -> 1
count_str -> String.to_integer(count_str)
end
Emulator.move_cursor_down(emulator, count)
end
# handle_cursor_forward/2
def handle_cursor_forward(params, emulator) do
count =
case params do
"" -> 1
count_str -> String.to_integer(count_str)
end
Emulator.move_cursor_forward(emulator, count)
end
# handle_cursor_back/2
def handle_cursor_back(params, emulator) do
count =
case params do
"" -> 1
count_str -> String.to_integer(count_str)
end
Emulator.move_cursor_back(emulator, count)
end
# handle_ed_command/2
def handle_ed_command(params, emulator) do
mode =
case params do
"" ->
0
mode_str ->
case Integer.parse(mode_str) do
{val, _} -> val
:error -> 0
end
end
case mode do
0 ->
Raxol.Terminal.Operations.ScreenOperations.erase_in_display(emulator, 0)
1 ->
Raxol.Terminal.Operations.ScreenOperations.erase_in_display(emulator, 1)
2 ->
Raxol.Terminal.Operations.ScreenOperations.erase_in_display(emulator, 2)
_ ->
emulator
end
end
# handle_el_command/2
def handle_el_command(params, emulator) do
mode =
case params do
"" ->
0
mode_str ->
case Integer.parse(mode_str) do
{val, _} -> val
:error -> 0
end
end
case mode do
0 -> Raxol.Terminal.Operations.ScreenOperations.erase_in_line(emulator, 0)
1 -> Raxol.Terminal.Operations.ScreenOperations.erase_in_line(emulator, 1)
2 -> Raxol.Terminal.Operations.ScreenOperations.erase_in_line(emulator, 2)
_ -> emulator
end
end
# handle_set_scroll_region/2
def handle_set_scroll_region(params, emulator) do
case String.split(params, ";") do
[top_str, bottom_str] ->
top = String.to_integer(top_str)
bottom = String.to_integer(bottom_str)
%{emulator | scroll_region: {top - 1, bottom - 1}}
[""] ->
%{emulator | scroll_region: nil}
_ ->
emulator
end
end
# handle_set_mode/2
def handle_set_mode(params, emulator) do
case parse_mode_params(params) do
[mode_code] -> try_set_mode(emulator, mode_code, true)
_ -> emulator
end
end
# handle_reset_mode/2
def handle_reset_mode(params, emulator) do
case parse_mode_params(params) do
[mode_code] -> try_set_mode(emulator, mode_code, false)
_ -> emulator
end
end
# handle_set_standard_mode/2
def handle_set_standard_mode(params, emulator) do
case parse_mode_params(params) do
[mode_code] ->
case lookup_standard_mode(mode_code) do
{:ok, mode_name} ->
set_mode_in_manager(emulator, mode_name, true)
_ ->
emulator
end
_ ->
emulator
end
end
# handle_reset_standard_mode/2
def handle_reset_standard_mode(params, emulator) do
case parse_mode_params(params) do
[mode_code] ->
case lookup_standard_mode(mode_code) do
{:ok, mode_name} ->
set_mode_in_manager(emulator, mode_name, false)
_ ->
emulator
end
_ ->
emulator
end
end
# handle_esc_equals/1
def handle_esc_equals(emulator) do
set_mode_in_manager(emulator, :decckm, true)
end
# handle_esc_greater/1
def handle_esc_greater(emulator) do
set_mode_in_manager(emulator, :decckm, false)
end
# handle_sgr/2
def handle_sgr(params, emulator) do
updated_style =
Raxol.Terminal.ANSI.SGRProcessor.handle_sgr(params, emulator.style)
log_sgr_debug("DEBUG: handle_sgr - emulator.style before: #{inspect(emulator.style)}")
log_sgr_debug("DEBUG: handle_sgr - updated_style: #{inspect(updated_style)}")
result = %{emulator | style: updated_style}
log_sgr_debug("DEBUG: handle_sgr - emulator.style after: #{inspect(result.style)}")
result
end
# handle_csi_general/4
def handle_csi_general(params, final_byte, emulator, intermediates \\ "") do
handle_csi_command(final_byte, params, emulator, intermediates)
end
# handle_device_attributes/3
def handle_device_attributes(params, emulator, intermediates) do
param_list = parse_params(params)
case {intermediates, param_list} do
{">", []} ->
# CSI > c or CSI > 0 c (Secondary DA)
response = "\e[>0;0;0c"
%{emulator | output_buffer: emulator.output_buffer <> response}
{">", [0]} ->
# CSI > 0 c (Secondary DA)
response = "\e[>0;0;0c"
%{emulator | output_buffer: emulator.output_buffer <> response}
{"", []} ->
# CSI c (Primary DA)
response = "\e[?6c"
%{emulator | output_buffer: emulator.output_buffer <> response}
{"", [0]} ->
# CSI 0 c (Primary DA)
response = "\e[?6c"
%{emulator | output_buffer: emulator.output_buffer <> response}
_ ->
# Ignore all other params (including [1], [1, ...], etc)
emulator
end
end
# handle_device_status_report/2
def handle_device_status_report(params, emulator) do
# Parse parameters
param_list = parse_params(params)
case param_list do
[5] ->
# DSR 5n - Report device status (OK)
# ESC [ 0 n (ready, no malfunctions)
response = "\e[0n"
%{emulator | output_buffer: emulator.output_buffer <> response}
[] ->
# DSR with no parameters - Report device status (OK)
# ESC [ 0 n (ready, no malfunctions)
response = "\e[0n"
%{emulator | output_buffer: emulator.output_buffer <> response}
[6] ->
# DSR 6n - Report cursor position
# ESC [ row ; col R
{row, col} = Raxol.Terminal.Cursor.Manager.get_position(emulator.cursor)
response = "\e[#{row + 1};#{col + 1}R"
%{emulator | output_buffer: emulator.output_buffer <> response}
_ ->
# Unknown parameter, ignore
emulator
end
end
# parse_params/1 - Helper to parse parameter string into list of integers
defp parse_params(params) when is_list(params), do: params
defp parse_params(params) when is_binary(params) do
params
|> String.split(";")
|> Enum.map(&String.trim/1)
|> Enum.filter(&(&1 != ""))
|> Enum.map(fn param ->
case Integer.parse(param) do
{val, _} -> val
:error -> 0
end
end)
end
defp parse_params(_), do: []
# Private helper functions
defp parse_mode_params(params) do
params
|> String.split(";")
|> Enum.map(&String.trim/1)
|> Enum.filter(&(&1 != ""))
|> Enum.map(&String.to_integer/1)
end
defp lookup_mode(mode_code) do
case Raxol.Terminal.ModeManager.lookup_private(mode_code) do
nil -> :error
mode_name -> {:ok, mode_name}
end
end
defp lookup_standard_mode(mode_code) do
case Raxol.Terminal.ModeManager.lookup_standard(mode_code) do
nil -> :error
mode_name -> {:ok, mode_name}
end
end
defp set_mode_in_manager(emulator, mode_name, value) do
case value do
true ->
case Raxol.Terminal.ModeManager.set_mode(emulator, [mode_name]) do
{:ok, new_emulator} ->
handle_screen_buffer_switch(new_emulator, mode_name, value)
{:error, _} ->
emulator
end
false ->
case Raxol.Terminal.ModeManager.reset_mode(emulator, [mode_name]) do
{:ok, new_emulator} ->
handle_screen_buffer_switch(new_emulator, mode_name, value)
{:error, _} ->
emulator
end
end
end
defp handle_screen_buffer_switch(emulator, mode, true)
when mode in [:alt_screen_buffer, :dec_alt_screen_save] do
alt_buf =
emulator.alternate_screen_buffer ||
Raxol.Terminal.ScreenBuffer.new(emulator.width, emulator.height)
Raxol.Terminal.Cursor.Manager.set_position(emulator.cursor, {0, 0})
%{
emulator
| active_buffer_type: :alternate,
alternate_screen_buffer: alt_buf
}
end
defp handle_screen_buffer_switch(emulator, mode, false)
when mode in [:alt_screen_buffer, :dec_alt_screen_save] do
Raxol.Terminal.Cursor.Manager.set_position(emulator.cursor, {0, 0})
%{emulator | active_buffer_type: :main}
end
defp handle_screen_buffer_switch(emulator, _mode, _value), do: emulator
defp log_sgr_debug(_msg) do
# Disabled for performance - uncomment for debugging
# File.write!(".tmp/sgr_debug.log", msg <> "\n", [:append])
:ok
end
# Additional CSI command handlers that delegate to CSIHandler
defp handle_erase_character(params, emulator, _intermediates) do
Raxol.Terminal.Commands.CSIHandler.handle_csi_sequence(
emulator,
"X",
params
)
|> extract_emulator()
end
defp handle_cursor_next_line(params, emulator, _intermediates) do
# Move cursor down N lines and to beginning of line
count =
case params do
[] -> 1
[n] when is_integer(n) -> max(1, n)
_ -> 1
end
# Move down count lines
emulator = Emulator.move_cursor_down(emulator, count)
# Move to beginning of line
%{emulator | cursor: %{emulator.cursor | col: 0}}
end
defp handle_cursor_previous_line(params, emulator, _intermediates) do
# Move cursor up N lines and to beginning of line
count =
case params do
[] -> 1
[n] when is_integer(n) -> max(1, n)
_ -> 1
end
# Move up count lines
emulator = Emulator.move_cursor_up(emulator, count)
# Move to beginning of line
%{emulator | cursor: %{emulator.cursor | col: 0}}
end
defp handle_cursor_horizontal_absolute(params, emulator, _intermediates) do
# Move cursor to absolute column position
col =
case params do
[] -> 1
[n] when is_integer(n) -> max(1, n)
_ -> 1
end
# Convert to 0-based and clamp to screen width
new_col = min(col - 1, emulator.width - 1)
%{emulator | cursor: %{emulator.cursor | col: new_col}}
end
defp handle_cursor_vertical_absolute(params, emulator, _intermediates) do
# Move cursor to absolute row position
row =
case params do
[] -> 1
[n] when is_integer(n) -> max(1, n)
_ -> 1
end
# Convert to 0-based and clamp to screen height
new_row = min(row - 1, emulator.height - 1)
%{emulator | cursor: %{emulator.cursor | row: new_row}}
end
defp handle_insert_lines(params, emulator, _intermediates) do
# Insert N blank lines at cursor position
count =
case params do
[] -> 1
[n] when is_integer(n) -> max(1, n)
_ -> 1
end
Raxol.Terminal.Commands.Screen.insert_lines(emulator, count)
end
defp handle_delete_lines(params, emulator, _intermediates) do
# Delete N lines at cursor position
count =
case params do
[] -> 1
[n] when is_integer(n) -> max(1, n)
_ -> 1
end
Raxol.Terminal.Commands.Screen.delete_lines(emulator, count)
end
defp handle_insert_characters(params, emulator, _intermediates) do
# Insert N blank characters at cursor position
count =
case params do
[] -> 1
[n] when is_integer(n) -> max(1, n)
_ -> 1
end
{cursor_y, cursor_x} =
Raxol.Terminal.Cursor.Manager.get_position(emulator.cursor)
buffer = Raxol.Terminal.Emulator.get_screen_buffer(emulator)
updated_buffer =
Raxol.Terminal.Buffer.CharEditor.insert_characters(
buffer,
cursor_y,
cursor_x,
count,
emulator.style
)
Raxol.Terminal.Emulator.update_active_buffer(emulator, updated_buffer)
end
defp handle_delete_characters(params, emulator, _intermediates) do
# Delete N characters at cursor position
count =
case params do
[] -> 1
[n] when is_integer(n) -> max(1, n)
_ -> 1
end
{cursor_y, cursor_x} =
Raxol.Terminal.Cursor.Manager.get_position(emulator.cursor)
buffer = Raxol.Terminal.Emulator.get_screen_buffer(emulator)
updated_buffer =
Raxol.Terminal.Buffer.CharEditor.delete_characters(
buffer,
cursor_y,
cursor_x,
count,
emulator.style
)
Raxol.Terminal.Emulator.update_active_buffer(emulator, updated_buffer)
end
defp handle_scroll_up(params, emulator, _intermediates) do
# Scroll up N lines
_count =
case params do
[] -> 1
[n] when is_integer(n) -> max(1, n)
_ -> 1
end
# NOTE: Scrolling not yet implemented. Future enhancement will add
# viewport scrolling support for handling scroll regions and history.
emulator
end
defp handle_scroll_down(params, emulator, _intermediates) do
# Scroll down N lines
_count =
case params do
[] -> 1
[n] when is_integer(n) -> max(1, n)
_ -> 1
end
# NOTE: Scrolling not yet implemented. Future enhancement will add
# viewport scrolling support for handling scroll regions and history.
emulator
end
defp handle_set_mode(params, emulator, intermediates) do
Raxol.Terminal.Commands.CSIHandler.handle_h_or_l(
emulator,
params,
intermediates,
?h
)
|> extract_emulator()
end
defp handle_reset_mode(params, emulator, intermediates) do
Raxol.Terminal.Commands.CSIHandler.handle_h_or_l(
emulator,
params,
intermediates,
?l
)
|> extract_emulator()
end
defp handle_tab_clear(params, emulator, _intermediates) do
# Clear tab stops
# params: [] or [0] = clear at cursor, [3] = clear all
case params do
[] -> emulator
[0] -> emulator
[3] -> emulator
_ -> emulator
end
end
defp extract_emulator({:ok, emulator}), do: emulator
defp extract_emulator({:error, _, emulator}), do: emulator
defp extract_emulator(emulator), do: emulator
defp handle_csi_command(final_byte, params, emulator, intermediates) do
require Raxol.Core.Runtime.Log
Raxol.Core.Runtime.Log.debug(
"handle_csi_command: final_byte=#{inspect(final_byte)}, params=#{inspect(params)}"
)
case csi_handlers()[final_byte] do
{handler, _arity} ->
Raxol.Core.Runtime.Log.debug(
"Found handler for #{inspect(final_byte)}: #{inspect(handler)}"
)
apply(handler, [params, emulator, intermediates])
nil ->
Raxol.Core.Runtime.Log.debug("No handler found for #{inspect(final_byte)}")
emulator
end
end
defp try_set_mode(emulator, mode_code, value) do
case lookup_standard_mode(mode_code) do
{:ok, mode_name} -> set_mode_in_manager(emulator, mode_name, value)
:error -> try_private_mode(emulator, mode_code, value)
end
end
defp try_private_mode(emulator, mode_code, value) do
case lookup_mode(mode_code) do
{:ok, mode_name} -> set_mode_in_manager(emulator, mode_name, value)
:error -> emulator
end
end
defp csi_handlers do
%{
# Erase commands
"J" => {&handle_ed_command/2, 2},
"K" => {&handle_el_command/2, 2},
"X" => {&handle_erase_character/3, 3},
# Cursor movement
"H" => {&handle_cursor_position/2, 2},
# HVP same as CUP
"f" => {&handle_cursor_position/2, 2},
"A" => {&handle_cursor_up/2, 2},
"B" => {&handle_cursor_down/2, 2},
"C" => {&handle_cursor_forward/2, 2},
"D" => {&handle_cursor_back/2, 2},
"E" => {&handle_cursor_next_line/3, 3},
"F" => {&handle_cursor_previous_line/3, 3},
"G" => {&handle_cursor_horizontal_absolute/3, 3},
"d" => {&handle_cursor_vertical_absolute/3, 3},
# Line operations
"L" => {&handle_insert_lines/3, 3},
"M" => {&handle_delete_lines/3, 3},
# Character operations
"@" => {&handle_insert_characters/3, 3},
"P" => {&handle_delete_characters/3, 3},
# Scroll operations
"S" => {&handle_scroll_up/3, 3},
"T" => {&handle_scroll_down/3, 3},
# Device commands
"c" => {&handle_device_attributes/3, 3},
"n" => {&handle_device_status_report/2, 2},
# Mode commands
"h" => {&handle_set_mode/3, 3},
"l" => {&handle_reset_mode/3, 3},
# Text formatting (already defined as public function)
"m" => {&handle_sgr/2, 2},
# Tab commands
"g" => {&handle_tab_clear/3, 3}
}
end
end