defmodule Raxol.Terminal.Parser.State.Manager do @moduledoc """ Manages the state of the terminal parser, including escape sequences, control sequences, and parser modes. """ defstruct [ :state, :params, :intermediate, :ignore, :osc_buffer, :dcs_buffer, :apc_buffer, :pm_buffer, :sos_buffer, :string_buffer, :string_terminator, :string_flags, :string_parser_state, :params_buffer, :intermediates_buffer, :payload_buffer, :final_byte, :designating_gset, :single_shift ] @type parser_state :: :ground | :escape | :csi_entry | :csi_param | :csi_intermediate | :csi_ignore | :osc_string | :dcs_entry | :dcs_param | :dcs_intermediate | :dcs_passthrough | :apc_string | :pm_string | :sos_string | :string @type params :: [non_neg_integer()] @type intermediate :: [non_neg_integer()] @type string_flags :: %{String.t() => boolean()} @type t :: %__MODULE__{ state: parser_state(), params: params(), intermediate: intermediate(), ignore: boolean(), osc_buffer: String.t(), dcs_buffer: String.t(), apc_buffer: String.t(), pm_buffer: String.t(), sos_buffer: String.t(), string_buffer: String.t(), string_terminator: non_neg_integer() | nil, string_flags: string_flags(), string_parser_state: parser_state() | nil } # Constants for character ranges @c0_range 0x00..0x1F @c1_range 0x80..0x9F @printable_range 0x20..0x7E @extended_range 0xA0..0xFF # Special characters @esc 0x1B @bel 0x07 @st 0x9C @osc 0x9D @pm 0x9E @apc 0x9F @csi 0x9B @dcs 0x90 # State handlers map defp state_handlers do %{ ground: &process_ground_state/2, escape: &process_escape_state/2, csi_entry: &process_csi_entry_state/2, csi_param: &process_csi_param_state/2, csi_intermediate: &process_csi_intermediate_state/2, csi_ignore: &process_csi_ignore_state/2, osc_string: &process_osc_string_state/2, dcs_entry: &process_dcs_entry_state/2, dcs_param: &process_dcs_param_state/2, dcs_intermediate: &process_dcs_intermediate_state/2, dcs_passthrough: &process_dcs_passthrough_state/2, apc_string: &process_apc_string_state/2, pm_string: &process_pm_string_state/2, sos_string: &process_sos_string_state/2, string: &process_string_state/2 } end @doc """ Creates a new parser state manager instance. """ def new do %__MODULE__{ state: :ground, params: [], intermediate: [], ignore: false, osc_buffer: "", dcs_buffer: "", apc_buffer: "", pm_buffer: "", sos_buffer: "", string_buffer: "", string_terminator: nil, string_flags: %{}, string_parser_state: nil, params_buffer: "", intermediates_buffer: "", payload_buffer: "", final_byte: nil, designating_gset: nil, single_shift: nil } end @doc """ Processes a single character and updates the parser state accordingly. """ def process_char(%__MODULE__{} = manager, char) when is_integer(char) do handler = Map.get(state_handlers(), manager.state, &process_ground_state/2) handler.(manager, char) end # State processing functions defp process_ground_state(manager, char) when char in @c0_range, do: handle_c0_control(manager, char) defp process_ground_state(manager, char) when char in @c1_range, do: handle_c1_control(manager, char) defp process_ground_state(manager, char) when char in @printable_range, do: handle_printable(manager, char) defp process_ground_state(manager, char) when char in @extended_range, do: handle_extended(manager, char) defp process_ground_state(manager, _char), do: manager defp process_escape_state(manager, char) when char in @c0_range, do: handle_c0_control(manager, char) defp process_escape_state(manager, char) when char in @c1_range, do: handle_c1_control(manager, char) defp process_escape_state(manager, char) when char in @printable_range, do: handle_escape_printable(manager, char) defp process_escape_state(manager, char) when char in @extended_range, do: handle_escape_extended(manager, char) defp process_escape_state(manager, _char), do: manager defp process_csi_entry_state(manager, char) when char in 0x30..0x3F//1, do: set_state(manager, :csi_param) defp process_csi_entry_state(manager, char) when char in 0x20..0x2F//1, do: set_state(manager, :csi_intermediate) defp process_csi_entry_state(manager, char) when char in 0x40..0x7E//1, do: set_state(manager, :ground) defp process_csi_entry_state(manager, _char), do: set_state(manager, :csi_ignore) defp process_csi_param_state(manager, char) when char in 0x30..0x3F//1, do: manager defp process_csi_param_state(manager, char) when char in 0x20..0x2F//1, do: set_state(manager, :csi_intermediate) defp process_csi_param_state(manager, char) when char in 0x40..0x7E//1, do: set_state(manager, :ground) defp process_csi_param_state(manager, _char), do: set_state(manager, :csi_ignore) defp process_csi_intermediate_state(manager, char) when char in 0x20..0x2F//1, do: manager defp process_csi_intermediate_state(manager, char) when char in 0x40..0x7E//1, do: set_state(manager, :ground) defp process_csi_intermediate_state(manager, _char), do: set_state(manager, :csi_ignore) defp process_csi_ignore_state(manager, char) when char in 0x40..0x7E//1, do: set_state(manager, :ground) defp process_csi_ignore_state(manager, _char), do: manager defp process_osc_string_state(manager, @bel), do: set_state(manager, :ground) defp process_osc_string_state(manager, @st), do: set_state(manager, :ground) defp process_osc_string_state(manager, char), do: set_osc_buffer(manager, manager.osc_buffer <> <>) defp process_dcs_entry_state(manager, char) when char in 0x30..0x3F//1, do: set_state(manager, :dcs_param) defp process_dcs_entry_state(manager, char) when char in 0x20..0x2F//1, do: set_state(manager, :dcs_intermediate) defp process_dcs_entry_state(manager, char) when char in 0x40..0x7E//1, do: set_state(manager, :dcs_passthrough) defp process_dcs_entry_state(manager, _char), do: set_state(manager, :ground) defp process_dcs_param_state(manager, char) when char in 0x30..0x3F//1, do: manager defp process_dcs_param_state(manager, char) when char in 0x20..0x2F//1, do: set_state(manager, :dcs_intermediate) defp process_dcs_param_state(manager, char) when char in 0x40..0x7E//1, do: set_state(manager, :dcs_passthrough) defp process_dcs_param_state(manager, _char), do: set_state(manager, :ground) defp process_dcs_intermediate_state(manager, char) when char in 0x20..0x2F//1, do: manager defp process_dcs_intermediate_state(manager, char) when char in 0x40..0x7E//1, do: set_state(manager, :dcs_passthrough) defp process_dcs_intermediate_state(manager, _char), do: set_state(manager, :ground) defp process_dcs_passthrough_state(manager, @st), do: set_state(manager, :ground) defp process_dcs_passthrough_state(manager, char), do: set_dcs_buffer(manager, manager.dcs_buffer <> <>) defp process_apc_string_state(manager, @bel), do: set_state(manager, :ground) defp process_apc_string_state(manager, @st), do: set_state(manager, :ground) defp process_apc_string_state(manager, char), do: set_apc_buffer(manager, manager.apc_buffer <> <>) defp process_pm_string_state(manager, @bel), do: set_state(manager, :ground) defp process_pm_string_state(manager, @st), do: set_state(manager, :ground) defp process_pm_string_state(manager, char), do: set_pm_buffer(manager, manager.pm_buffer <> <>) defp process_sos_string_state(manager, @bel), do: set_state(manager, :ground) defp process_sos_string_state(manager, @st), do: set_state(manager, :ground) defp process_sos_string_state(manager, char), do: set_sos_buffer(manager, manager.sos_buffer <> <>) defp process_string_state(manager, char) do case char do c when c == manager.string_terminator -> set_state(manager, :ground) c -> set_string_buffer(manager, manager.string_buffer <> <>) end end # Helper functions for handling different character types defp handle_c0_control(manager, char) do case char do @esc -> set_state(manager, :escape) @bel -> manager @st -> manager _ -> manager end end defp handle_c1_control(manager, char) do case char do @osc -> set_state(manager, :osc_string) @pm -> set_state(manager, :pm_string) @apc -> set_state(manager, :apc_string) @csi -> set_state(manager, :csi_entry) @dcs -> set_state(manager, :dcs_entry) _ -> manager end end defp handle_printable(manager, _char) do manager end defp handle_extended(manager, _char) do manager end defp handle_escape_printable(manager, char) do case char do _ when char in 0x30..0x7E//1 -> set_state(manager, :ground) _ -> manager end end defp handle_escape_extended(manager, _char) do set_state(manager, :ground) end @doc """ Gets the current parser state. """ def get_state(%__MODULE__{} = manager) do manager.state end @doc """ Sets the parser state. """ def set_state(%__MODULE__{} = manager, state) when state in [ :ground, :escape, :csi_entry, :csi_param, :csi_intermediate, :csi_ignore, :osc_string, :dcs_entry, :dcs_param, :dcs_intermediate, :dcs_passthrough, :apc_string, :pm_string, :sos_string, :string ] do %{manager | state: state} end @doc """ Gets the current parameters. """ def get_params(%__MODULE__{} = manager) do manager.params end @doc """ Sets the parameters. """ def set_params(%__MODULE__{} = manager, params) when is_list(params) do %{manager | params: params} end @doc """ Gets the current intermediate characters. """ def get_intermediate(%__MODULE__{} = manager) do manager.intermediate end @doc """ Sets the intermediate characters. """ def set_intermediate(%__MODULE__{} = manager, intermediate) when is_list(intermediate) do %{manager | intermediate: intermediate} end @doc """ Checks if the parser is in ignore mode. """ def ignore?(%__MODULE__{} = manager) do manager.ignore end @doc """ Sets the ignore mode. """ def set_ignore(%__MODULE__{} = manager, ignore) when is_boolean(ignore) do %{manager | ignore: ignore} end @doc """ Gets the OSC buffer content. """ def get_osc_buffer(%__MODULE__{} = manager) do manager.osc_buffer end @doc """ Sets the OSC buffer content. """ def set_osc_buffer(%__MODULE__{} = manager, content) when is_binary(content) do %{manager | osc_buffer: content} end @doc """ Gets the DCS buffer content. """ def get_dcs_buffer(%__MODULE__{} = manager) do manager.dcs_buffer end @doc """ Sets the DCS buffer content. """ def set_dcs_buffer(%__MODULE__{} = manager, content) when is_binary(content) do %{manager | dcs_buffer: content} end @doc """ Gets the APC buffer content. """ def get_apc_buffer(%__MODULE__{} = manager) do manager.apc_buffer end @doc """ Sets the APC buffer content. """ def set_apc_buffer(%__MODULE__{} = manager, content) when is_binary(content) do %{manager | apc_buffer: content} end @doc """ Gets the PM buffer content. """ def get_pm_buffer(%__MODULE__{} = manager) do manager.pm_buffer end @doc """ Sets the PM buffer content. """ def set_pm_buffer(%__MODULE__{} = manager, content) when is_binary(content) do %{manager | pm_buffer: content} end @doc """ Gets the SOS buffer content. """ def get_sos_buffer(%__MODULE__{} = manager) do manager.sos_buffer end @doc """ Sets the SOS buffer content. """ def set_sos_buffer(%__MODULE__{} = manager, content) when is_binary(content) do %{manager | sos_buffer: content} end @doc """ Gets the string buffer content. """ def get_string_buffer(%__MODULE__{} = manager) do manager.string_buffer end @doc """ Sets the string buffer content. """ def set_string_buffer(%__MODULE__{} = manager, content) when is_binary(content) do %{manager | string_buffer: content} end @doc """ Gets the string terminator. """ def get_string_terminator(%__MODULE__{} = manager) do manager.string_terminator end @doc """ Sets the string terminator. """ def set_string_terminator(%__MODULE__{} = manager, terminator) when is_integer(terminator) do %{manager | string_terminator: terminator} end @doc """ Gets the string flags. """ def get_string_flags(%__MODULE__{} = manager) do manager.string_flags end @doc """ Sets the string flags. """ def set_string_flags(%__MODULE__{} = manager, flags) when is_map(flags) do %{manager | string_flags: flags} end @doc """ Gets the string parser state. """ def get_string_parser_state(%__MODULE__{} = manager) do manager.string_parser_state end @doc """ Sets the string parser state. """ def set_string_parser_state(%__MODULE__{} = manager, state) when state in [ :ground, :escape, :csi_entry, :csi_param, :csi_intermediate, :csi_ignore, :osc_string, :dcs_entry, :dcs_param, :dcs_intermediate, :dcs_passthrough, :apc_string, :pm_string, :sos_string, :string ] do %{manager | string_parser_state: state} end @doc """ Clears all string buffers. """ def clear_string_buffers(%__MODULE__{} = manager) do %{ manager | osc_buffer: "", dcs_buffer: "", apc_buffer: "", pm_buffer: "", sos_buffer: "", string_buffer: "", string_terminator: nil, string_flags: %{}, string_parser_state: nil } end @doc """ Resets the parser state manager to its initial state. """ def reset(%__MODULE__{} = _manager) do new() end # Functions expected by tests def get_current_state(manager) do manager end def transition_to(manager, :csi_entry) do %{ manager | state: :csi_entry, params_buffer: "", intermediates_buffer: "" } end def transition_to(manager, :osc_string) do %{manager | state: :osc_string, payload_buffer: ""} end def transition_to(manager, :dcs_entry) do %{ manager | state: :dcs_entry, params_buffer: "", intermediates_buffer: "", payload_buffer: "" } end def transition_to(manager, _) do %{manager | state: :ground} end def append_param(manager, param) do %{manager | params_buffer: manager.params_buffer <> param} end def append_intermediate(manager, intermediate) do append = convert_intermediate_to_string(intermediate) %{manager | intermediates_buffer: manager.intermediates_buffer <> append} end defp convert_intermediate_to_string(intermediate) when is_integer(intermediate) do <> end defp convert_intermediate_to_string(intermediate) when is_binary(intermediate) do intermediate end defp convert_intermediate_to_string([single]) when is_integer(single) do <> end defp convert_intermediate_to_string(intermediate) when is_list(intermediate) do to_string(intermediate) end defp convert_intermediate_to_string(_) do "" end def append_payload(manager, payload) do %{manager | payload_buffer: manager.payload_buffer <> payload} end def set_final_byte(manager, byte) do %{manager | final_byte: byte} end def set_designating_gset(manager, gset) do %{manager | designating_gset: gset} end def process_input(emulator, state, input) do handler = Map.get(state_handlers_input(), state.state, &handle_default_state/3) handler.(emulator, state, input) end defp handle_default_state(emulator, state, input) do {:continue, emulator, %{state | state: :ground}, input} end defp state_handlers_input do %{ ground: &handle_ground_state/3, escape: &handle_escape_state/3 } end defp handle_ground_state(emulator, state, input) do case detect_single_shift(input) do {:ss2, next} -> handle_single_shift(emulator, state, :ss2, next) {:ss3, next} -> handle_single_shift(emulator, state, :ss3, next) :none -> handle_single_shift_consumption(emulator, state, input) end end defp handle_escape_state(emulator, state, input) do case input do <<"N", rest::binary>> -> # ESC N (SS2) - Single Shift 2 {:continue, emulator, %{state | state: :ground, single_shift: :ss2}, rest} <<"O", rest::binary>> -> # ESC O (SS3) - Single Shift 3 {:continue, emulator, %{state | state: :ground, single_shift: :ss3}, rest} <<"[", rest::binary>> -> # ESC [ - CSI entry {:continue, emulator, %{state | state: :csi_entry}, rest} _ -> # Default: transition to ground state {:continue, emulator, %{state | state: :ground}, input} end end defp detect_single_shift(<<142, next::binary>>), do: {:ss2, next} defp detect_single_shift(<<143, next::binary>>), do: {:ss3, next} defp detect_single_shift(_), do: :none defp handle_single_shift(emulator, state, shift_type, <>) do # When there's a character after SS2/SS3, set the shift and then process the character # This matches the expected behavior: SS2/SS3 affects the next character only state_with_shift = %{state | single_shift: shift_type} handle_single_shift_consumption( emulator, state_with_shift, <> ) end defp handle_single_shift(emulator, state, shift_type, _) do # When SS2/SS3 is at the end of input, set the shift for the next character {:continue, emulator, %{state | single_shift: shift_type}, ""} end defp handle_single_shift_consumption(emulator, state, input) do case {state.single_shift, input} do {nil, _} -> {:continue, emulator, state, input} {_shift, <<>>} -> # No input to process, keep the shift {:continue, emulator, state, input} {_shift, <<_byte, rest::binary>>} -> # Process the first byte, clear the shift, reset state to :ground, and return the rest {:continue, emulator, %{state | single_shift: nil, state: :ground}, rest} end end end