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/executor.ex
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