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/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