Packages
Multi-surface application runtime for Elixir. One TEA module renders to terminal, browser (LiveView), SSH, and MCP (agents). 30+ widgets, flexbox + CSS grid, AI agent runtime, distributed swarm with CRDTs, time-travel debugging, session recording, sandboxed REPL, and agentic commerce.
Current section
Files
Jump to
Current section
Files
lib/raxol/terminal/parser/state/parser_state_manager.ex
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 <> <<char>>)
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 <> <<char>>)
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 <> <<char>>)
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 <> <<char>>)
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 <> <<char>>)
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 <> <<c>>)
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
<<intermediate>>
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
<<single>>
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, <<char, rest::binary>>) 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,
<<char, rest::binary>>
)
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