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