defmodule Raxol.Terminal.ANSI.SequenceHandler do @moduledoc """ Handles parsing and processing of ANSI escape sequences. This module extracts the ANSI sequence parsing logic from the main emulator. """ @doc """ Parses ANSI sequences from input and returns the parsed result. """ @spec parse_ansi_sequence(binary()) :: {:incomplete, nil} | tuple() def parse_ansi_sequence(rest) do # Disabled for performance # File.write!( # ".tmp/parse_ansi_sequence.log", # "parse_ansi_sequence input: #{inspect(rest)}\n", # [:append] # ) case find_matching_parser(rest) do nil -> # Disabled for performance # File.write!( # ".tmp/parse_ansi_sequence.log", # "parse_ansi_sequence result: nil\n", # [:append] # ) {:incomplete, nil} result -> # Disabled for performance # File.write!( # ".tmp/parse_ansi_sequence.log", # "parse_ansi_sequence result: #{inspect(result)}\n", # [:append] # ) result end end @doc """ Parses OSC (Operating System Command) sequences. """ @spec parse_osc(binary()) :: {:osc, binary(), nil} | nil def parse_osc(<<0x1B, 0x5D, 0x30, 0x3B, remaining::binary>>) do case String.split(remaining, <<0x07>>, parts: 2) do [_title, rest] -> {:osc, rest, nil} _ -> nil end end def parse_osc(_), do: nil @doc """ Parses DCS (Device Control String) sequences. """ @spec parse_dcs(binary()) :: {:dcs, binary(), nil} | nil def parse_dcs(<<0x1B, 0x50, 0x30, 0x3B, remaining::binary>>) do case String.split(remaining, <<0x07>>, parts: 2) do [_params, rest] -> {:dcs, rest, nil} _ -> nil end end def parse_dcs(_), do: nil @doc """ Parses CSI cursor position sequences. """ @spec parse_csi_cursor_pos(binary()) :: {:csi_cursor_pos, binary(), binary(), nil} | nil def parse_csi_cursor_pos(<<0x1B, 0x5B, remaining::binary>>) do case String.split(remaining, <<0x48>>, parts: 2) do [params, rest] -> {:csi_cursor_pos, params, rest, nil} _ -> nil end end def parse_csi_cursor_pos(_), do: nil @doc """ Parses CSI cursor up sequences. """ @spec parse_csi_cursor_up(binary()) :: {:csi_cursor_up, binary(), binary(), nil} | nil def parse_csi_cursor_up(<<0x1B, 0x5B, remaining::binary>>) do case String.split(remaining, <<0x41>>, parts: 2) do [params, rest] -> {:csi_cursor_up, params, rest, nil} _ -> nil end end def parse_csi_cursor_up(_), do: nil @doc """ Parses CSI cursor down sequences. """ @spec parse_csi_cursor_down(binary()) :: {:csi_cursor_down, binary(), binary(), nil} | nil def parse_csi_cursor_down(<<0x1B, 0x5B, remaining::binary>>) do case String.split(remaining, <<0x42>>, parts: 2) do [params, rest] -> {:csi_cursor_down, params, rest, nil} _ -> nil end end def parse_csi_cursor_down(_), do: nil @doc """ Parses CSI cursor forward sequences. """ @spec parse_csi_cursor_forward(binary()) :: {:csi_cursor_forward, binary(), binary(), nil} | nil def parse_csi_cursor_forward(<<0x1B, 0x5B, remaining::binary>>) do case String.split(remaining, <<0x43>>, parts: 2) do [params, rest] -> {:csi_cursor_forward, params, rest, nil} _ -> nil end end def parse_csi_cursor_forward(_), do: nil @doc """ Parses CSI cursor back sequences. """ @spec parse_csi_cursor_back(binary()) :: {:csi_cursor_back, binary(), binary(), nil} | nil def parse_csi_cursor_back(<<0x1B, 0x5B, remaining::binary>>) do case String.split(remaining, <<0x44>>, parts: 2) do [params, rest] -> {:csi_cursor_back, params, rest, nil} _ -> nil end end def parse_csi_cursor_back(_), do: nil @doc """ Parses CSI cursor show sequences. """ @spec parse_csi_cursor_show(binary()) :: {:csi_cursor_show, binary(), nil} | nil def parse_csi_cursor_show(<<0x1B, 0x5B, 0x3F, 0x32, 0x35, 0x68, remaining::binary>>), do: {:csi_cursor_show, remaining, nil} def parse_csi_cursor_show(_), do: nil @doc """ Parses CSI cursor hide sequences. """ @spec parse_csi_cursor_hide(binary()) :: {:csi_cursor_hide, binary(), nil} | nil def parse_csi_cursor_hide(<<0x1B, 0x5B, 0x3F, 0x32, 0x35, 0x6C, remaining::binary>>), do: {:csi_cursor_hide, remaining, nil} def parse_csi_cursor_hide(_), do: nil @doc """ Parses CSI clear screen sequences. """ @spec parse_csi_clear_screen(binary()) :: {:csi_clear_screen, binary(), nil} | nil def parse_csi_clear_screen(<<0x1B, 0x5B, 0x32, 0x4A, remaining::binary>>), do: {:csi_clear_screen, remaining, nil} def parse_csi_clear_screen(_), do: nil @doc """ Parses CSI clear line sequences. """ @spec parse_csi_clear_line(binary()) :: {:csi_clear_line, binary(), nil} | nil def parse_csi_clear_line(<<0x1B, 0x5B, 0x32, 0x4B, remaining::binary>>), do: {:csi_clear_line, remaining, nil} def parse_csi_clear_line(_), do: nil @doc """ Parses CSI set mode sequences. """ @spec parse_csi_set_mode(binary()) :: {:csi_set_mode, binary(), binary(), nil} | nil def parse_csi_set_mode(<<0x1B, 0x5B, 0x3F, remaining::binary>>) do case String.split(remaining, "h", parts: 2) do [params, rest] -> {:csi_set_mode, params, rest, nil} _ -> nil end end def parse_csi_set_mode(_), do: nil @doc """ Parses CSI reset mode sequences. """ @spec parse_csi_reset_mode(binary()) :: {:csi_reset_mode, binary(), binary(), nil} | nil def parse_csi_reset_mode(<<0x1B, 0x5B, 0x3F, remaining::binary>>) do case String.split(remaining, "l", parts: 2) do [params, rest] -> {:csi_reset_mode, params, rest, nil} _ -> nil end end def parse_csi_reset_mode(_), do: nil @doc """ Parses CSI set standard mode sequences. """ @spec parse_csi_set_standard_mode(binary()) :: {:csi_set_standard_mode, binary(), binary(), nil} | nil def parse_csi_set_standard_mode(<<0x1B, 0x5B, remaining::binary>>) do case String.split(remaining, "h", parts: 2) do [params, rest] -> {:csi_set_standard_mode, params, rest, nil} _ -> nil end end def parse_csi_set_standard_mode(_), do: nil @doc """ Parses CSI reset standard mode sequences. """ @spec parse_csi_reset_standard_mode(binary()) :: {:csi_reset_standard_mode, binary(), binary(), nil} | nil def parse_csi_reset_standard_mode(<<0x1B, 0x5B, remaining::binary>>) do case String.split(remaining, "l", parts: 2) do [params, rest] -> {:csi_reset_standard_mode, params, rest, nil} _ -> nil end end def parse_csi_reset_standard_mode(_), do: nil @doc """ Parses ESC equals sequences. """ @spec parse_esc_equals(binary()) :: {:esc_equals, binary(), nil} | nil def parse_esc_equals(<<0x1B, 0x3D, remaining::binary>>), do: {:esc_equals, remaining, nil} def parse_esc_equals(_), do: nil @doc """ Parses ESC greater than sequences. """ @spec parse_esc_greater(binary()) :: {:esc_greater, binary(), nil} | nil def parse_esc_greater(<<0x1B, 0x3E, remaining::binary>>), do: {:esc_greater, remaining, nil} def parse_esc_greater(_), do: nil @doc """ Parses CSI set scroll region sequences. """ @spec parse_csi_set_scroll_region(binary()) :: {:csi_set_scroll_region, binary(), binary(), nil} | nil def parse_csi_set_scroll_region(<<0x1B, 0x5B, remaining::binary>>) do case String.split(remaining, "r", parts: 2) do [params, rest] when is_binary(params) -> {:csi_set_scroll_region, params, rest, nil} _ -> nil end end def parse_csi_set_scroll_region(_), do: nil @doc """ Parses CSI general sequences. """ @spec parse_csi_general(binary()) :: {:csi_general, binary(), binary(), binary(), binary()} | nil def parse_csi_general(<<0x1B, 0x5B, remaining::binary>>) do # Match all before final byte, then final byte, then rest case Regex.run(~r|^([^A-Za-z]*)([A-Za-z])(.*)|, remaining) do [_, before_final, final_byte, rest] -> # Reverse, take digits/;/: from end as params, rest as intermediates rev = String.reverse(before_final) {rev_params, rev_intermediates} = Regex.run(~r/^([\d;:]*)(.*)/, rev, capture: :all_but_first) |> case do [ps, ints] -> {ps, ints} _ -> {rev, ""} end params = String.reverse(rev_params) intermediates = String.reverse(rev_intermediates) {:csi_general, params, intermediates, final_byte, rest} _ -> nil end end def parse_csi_general(_), do: nil @doc """ Parses SGR (Select Graphic Rendition) sequences. """ @spec parse_sgr(binary()) :: {:sgr, binary(), binary(), nil} | nil def parse_sgr(<<0x1B, 0x5B, remaining::binary>>) do # Only match if the remaining part contains 'm' case String.split(remaining, "m", parts: 2) do [params, rest] when is_binary(params) -> # Validate that params contains only digits, semicolons, and colons, or is empty (reset) case params == "" or String.match?(params, ~r/^[\d;:]*$/) do true -> # Disabled for performance # File.write!( # ".tmp/parse_sgr.log", # "parse_sgr MATCH: params=#{inspect(params)}, rest=#{inspect(rest)}\n", # [:append] # ) {:sgr, params, rest, nil} false -> # Disabled for performance # File.write!( # ".tmp/parse_sgr.log", # "parse_sgr NO_MATCH: params=#{inspect(params)} (invalid format)\n", # [:append] # ) nil end _ -> # Disabled for performance # File.write!( # ".tmp/parse_sgr.log", # "parse_sgr NO_MATCH: no 'm' found in #{inspect(remaining)}\n", # [:append] # ) nil end end def parse_sgr(_input) do # Disabled for performance # File.write!( # ".tmp/parse_sgr.log", # "parse_sgr NO_MATCH: input=#{inspect(input)} (no ESC[)\n", # [:append] # ) nil end @doc """ Parses unknown escape sequences. """ @spec parse_unknown(binary()) :: {:unknown, binary(), nil} | nil def parse_unknown(<<0x1B, remaining::binary>>) do # Skip one character after ESC case remaining do <<_char, rest::binary>> -> {:unknown, rest, nil} _ -> nil end end def parse_unknown(_), do: nil @doc """ Parses mouse event sequences in the format ESC[M