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