defmodule Raxol.Terminal.Commands.Executor do @moduledoc false alias Raxol.Core.Runtime.Log alias Raxol.Terminal.Commands.CommandsParser, as: Parser alias Raxol.Terminal.Commands.CSIHandler alias Raxol.Terminal.Commands.DCSHandler alias Raxol.Terminal.Commands.OSCHandler alias Raxol.Terminal.Emulator @basic_commands [?m, ?H, ?r, ?J, ?K] @cursor_commands [?A, ?B, ?C, ?D, ?E, ?F, ?G, ?d] @screen_commands [?L, ?M, ?P, ?@, ?S, ?T, ?X] @device_commands [?c, ?n, ?s, ?u, ?t] @scs_commands [?(, ?), ?*, ?+] @command_map %{ basic: &CSIHandler.handle_basic_command/3, cursor: &CSIHandler.handle_cursor_command/3, screen: &CSIHandler.handle_screen_command/3, device: &CSIHandler.handle_device_command/4, mode: &CSIHandler.handle_h_or_l/4, scs: &CSIHandler.handle_scs/3, deccusr: &CSIHandler.handle_q_deccusr/2 } @command_types %{ basic: @basic_commands, cursor: @cursor_commands, screen: @screen_commands, device: @device_commands, scs: @scs_commands } @spec execute_csi_command( Emulator.t(), String.t(), String.t(), non_neg_integer() ) :: Emulator.t() def execute_csi_command( emulator, params_buffer, intermediates_buffer, final_byte ) do # Raxol.Core.Runtime.Log.debug( # "[Executor] BEFORE: scroll=#{inspect(emulator.scroll_region)}" # ) params = Parser.parse_params(params_buffer) result = dispatch_csi_command( emulator, params, intermediates_buffer, final_byte, params_buffer ) log_and_return_result(result) end defp dispatch_csi_command( emulator, params, intermediates_buffer, final_byte, params_buffer ) do case get_command_type(final_byte, intermediates_buffer) do {:ok, type} -> apply_handler( type, emulator, params, intermediates_buffer, final_byte, params_buffer ) :unknown -> log_unknown_csi(final_byte) {:error, :unhandled_csi, emulator} end end defp get_command_type(final_byte, intermediates_buffer) do case {final_byte, intermediates_buffer} do {?q, " "} -> {:ok, :deccusr} {fb, _} when fb in [?h, ?l] -> {:ok, :mode} {fb, _} -> find_command_type(fb) end end defp find_command_type(final_byte) do Enum.find_value(@command_types, :unknown, fn {type, commands} -> case final_byte in commands do true -> {:ok, type} false -> nil end end) end defp apply_handler( :basic, emulator, params, _intermediates, final_byte, _params_buffer ), do: @command_map.basic.(emulator, params, final_byte) defp apply_handler( :mode, emulator, params, intermediates, final_byte, _params_buffer ), do: @command_map.mode.(emulator, params, intermediates, final_byte) defp apply_handler( :device, emulator, params, intermediates, final_byte, _params_buffer ), do: @command_map.device.(emulator, params, intermediates, final_byte) defp apply_handler( :scs, emulator, _params, _intermediates, final_byte, params_buffer ), do: @command_map.scs.(emulator, params_buffer, final_byte) defp apply_handler( :deccusr, emulator, params, _intermediates, _final_byte, _params_buffer ), do: @command_map.deccusr.(emulator, params) defp apply_handler( :cursor, emulator, params, _intermediates, final_byte, _params_buffer ), do: @command_map.cursor.(emulator, params, final_byte) defp apply_handler( :screen, emulator, params, _intermediates, final_byte, _params_buffer ), do: @command_map.screen.(emulator, params, final_byte) defp log_and_return_result({:ok, emulator}) do emulator end defp log_and_return_result({:ok, emulator, _output}) do emulator end defp log_and_return_result({:error, reason}) do Log.error("Executor error: #{inspect(reason)}") {:error, reason} end defp log_and_return_result({:error, reason, emulator}) do Log.error("Executor error: #{inspect(reason)}") emulator end defp log_and_return_result(%Raxol.Terminal.Emulator{} = emulator) do emulator end defp log_and_return_result(%Raxol.Terminal.EmulatorLite{} = emulator) do emulator end defp log_unknown_csi(final_byte) do Raxol.Core.Runtime.Log.warning_with_context( "Unknown CSI command: #{inspect(final_byte)}", %{} ) end @spec execute_osc_command(Emulator.t(), String.t()) :: Emulator.t() def execute_osc_command(emulator, command_string) do Raxol.Core.Runtime.Log.debug("Executing OSC command: #{inspect(command_string)}") # handle_osc_command returns {:ok, emulator} or {:error, reason, emulator} case handle_osc_command(emulator, command_string) do {:ok, updated_emulator} -> updated_emulator {:error, _reason, updated_emulator} -> updated_emulator end end @spec execute_osc_command(Emulator.t(), integer(), list()) :: Emulator.t() def execute_osc_command(emulator, command, params) do # Convert params to string format for the existing handler command_string = "#{command};#{Enum.join(params, ";")}" execute_osc_command(emulator, command_string) end defp handle_osc_command(emulator, command_string) do with [ps_str, pt] <- String.split(command_string, ";", parts: 2), {ps_code, ""} <- Integer.parse(ps_str) do dispatch_osc_command(emulator, ps_code, pt) else _ -> Raxol.Core.Runtime.Log.warning_with_context( "OSC: Unexpected command format: \"#{command_string}\"", %{} ) {:error, :malformed_osc, emulator} end end defp dispatch_osc_command(emulator, ps_code, pt) do OSCHandler.handle(emulator, ps_code, pt) end @spec execute_dcs_command( Emulator.t(), String.t(), String.t(), integer(), String.t() ) :: Emulator.t() def execute_dcs_command( emulator, params_buffer, intermediates_buffer, final_byte, data_string ) do Raxol.Core.Runtime.Log.debug( "Executing DCS command: #{inspect(data_string)} with final_byte: #{final_byte}" ) handle_dcs_command( emulator, params_buffer, intermediates_buffer, final_byte, data_string ) end @spec execute_dcs_command(map(), binary(), binary(), binary()) :: {:ok, map()} | {:error, atom(), map()} def execute_dcs_command( emulator, params_buffer, intermediates_buffer, data_string ) do Raxol.Core.Runtime.Log.debug("Executing DCS command: #{inspect(data_string)}") handle_dcs_command( emulator, params_buffer, intermediates_buffer, data_string ) end @spec execute_dcs_command(map(), integer(), list()) :: {:ok, map()} | {:error, atom(), map()} def execute_dcs_command(emulator, _command, params) do # Convert to string format for the existing handler params_buffer = Enum.join(params, ";") execute_dcs_command(emulator, params_buffer, "", "") end defp handle_dcs_command( emulator, params_buffer, intermediates_buffer, final_byte, data_string ) do # Always use the 5-argument version of handle_dcs which properly handles all cases case DCSHandler.handle_dcs( emulator, params_buffer, intermediates_buffer, final_byte, data_string ) do {:ok, updated_emulator} -> updated_emulator {:error, _reason, fallback_emulator} -> fallback_emulator end end defp handle_dcs_command( emulator, params_buffer, intermediates_buffer, data_string ) do # Parse params_buffer to get the final byte final_byte = case String.last(params_buffer) do nil -> nil last_char -> :binary.last(last_char) end # Remove the final byte from params_buffer to get the actual params params_without_final = case String.length(params_buffer) do 0 -> "" _ -> String.slice(params_buffer, 0, String.length(params_buffer) - 1) end case intermediates_buffer do "" -> # Simple DCS command without intermediates DCSHandler.handle_dcs(emulator, params_without_final, data_string) _ -> # DCS command with intermediates - need to pass final byte case final_byte do nil -> Raxol.Core.Runtime.Log.warning_with_context( "DCS: No final byte found in params_buffer: \"#{params_buffer}\"", %{} ) {:error, :malformed_dcs, emulator} final_byte -> DCSHandler.handle_dcs( emulator, params_without_final, intermediates_buffer, final_byte, data_string ) end end end end