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
raxol lib raxol terminal session serializer.ex
Raw

lib/raxol/terminal/session/serializer.ex

defmodule Raxol.Terminal.Session.Serializer do
@moduledoc """
Handles serialization and deserialization of terminal session state.
Refactored version with pure functional error handling patterns.
All try/catch blocks have been replaced with with statements and proper error tuples.
"""
alias Raxol.Terminal.{Session, ScreenBuffer, Emulator}
alias Raxol.Terminal.ScreenBuffer.Core, as: ConsolidatedBuffer
require Raxol.Core.Runtime.Log
@doc """
Serializes a session state to a map that can be stored and later restored.
"""
@spec serialize(Session.t()) :: {:ok, map()} | {:error, term()}
def serialize(%Session{} = session) do
with {:ok, emulator_data} <- safe_serialize_emulator(session.emulator),
{:ok, renderer_data} <- safe_serialize_renderer(session.renderer) do
{:ok,
%{
id: session.id,
width: session.width,
height: session.height,
title: session.title,
theme: session.theme,
auto_save: session.auto_save,
emulator: emulator_data,
renderer: renderer_data
}}
else
{:error, reason} = error ->
Raxol.Core.Runtime.Log.error(
"Session serialization failed: #{inspect(reason)}"
)
error
end
end
@doc """
Serializes a session state to a map, returning the map directly for backward compatibility.
Falls back to empty session data on error.
"""
@spec serialize!(Session.t()) :: map()
def serialize!(%Session{} = session) do
case serialize(session) do
{:ok, data} ->
data
{:error, reason} ->
Raxol.Core.Runtime.Log.error(
"Session serialization failed, returning fallback: #{inspect(reason)}"
)
# Return minimal valid session data
%{
id: session.id,
width: session.width || 80,
height: session.height || 24,
title: session.title || "",
theme: session.theme || %{},
auto_save: session.auto_save || false,
emulator: %{},
renderer: %{}
}
end
end
@doc """
Deserializes a session state from a map.
"""
@spec deserialize(map()) :: {:ok, Session.t()} | {:error, term()}
def deserialize(%{
id: id,
width: width,
height: height,
title: title,
theme: theme,
auto_save: auto_save,
emulator: emulator_data,
renderer: renderer_data
}) do
with {:ok, emulator} <- deserialize_emulator(emulator_data),
{:ok, renderer} <- deserialize_renderer(renderer_data) do
session = %Session{
id: id,
width: width,
height: height,
title: title,
theme: theme,
auto_save: auto_save,
emulator: emulator,
renderer: renderer
}
{:ok, session}
end
end
def deserialize(invalid_data) do
Raxol.Core.Runtime.Log.error(
"Invalid session data: #{inspect(invalid_data)}"
)
{:error, :invalid_session_data}
end
# Safe serialization helpers with error handling
defp safe_serialize_emulator(%Emulator{} = emulator) do
with {:ok, main_buffer} <-
safe_serialize_screen_buffer(emulator.main_screen_buffer),
{:ok, alt_buffer} <-
safe_serialize_screen_buffer(emulator.alternate_screen_buffer),
{:ok, scrollback} <-
safe_serialize_screen_buffer(emulator.scrollback_buffer) do
{:ok,
%{
main_screen_buffer: main_buffer,
alternate_screen_buffer: alt_buffer,
active_buffer_type: emulator.active_buffer_type,
scrollback_buffer: scrollback,
cursor: emulator.cursor,
mode_manager: emulator.mode_manager,
style: emulator.style,
charset_state: emulator.charset_state,
width: emulator.width,
height: emulator.height,
window_state: emulator.window_state,
state_stack: emulator.state_stack,
output_buffer: emulator.output_buffer,
scrollback_limit: emulator.scrollback_limit,
window_title: emulator.window_title,
plugin_manager: emulator.plugin_manager,
saved_cursor: emulator.saved_cursor,
scroll_region: emulator.scroll_region,
sixel_state: emulator.sixel_state,
last_col_exceeded: emulator.last_col_exceeded,
cursor_blink_rate: emulator.cursor_blink_rate,
cursor_style: emulator.cursor_style,
session_id: emulator.session_id,
client_options: emulator.client_options
}}
else
{:error, reason} ->
Raxol.Core.Runtime.Log.error(
"Emulator serialization failed: #{inspect(reason)}"
)
{:error, {:emulator_serialization_failed, reason}}
end
end
defp safe_serialize_emulator(nil) do
{:ok, nil}
end
defp safe_serialize_emulator(invalid) do
Raxol.Core.Runtime.Log.error(
"Invalid emulator structure: #{inspect(invalid)}"
)
{:error, :invalid_emulator_structure}
end
defp safe_serialize_renderer(renderer) when is_map(renderer) do
with {:ok, buffer} <-
safe_serialize_screen_buffer(Map.get(renderer, :screen_buffer)) do
{:ok,
%{
screen_buffer: buffer,
theme: Map.get(renderer, :theme)
}}
else
{:error, reason} ->
Raxol.Core.Runtime.Log.error(
"Renderer serialization failed: #{inspect(reason)}"
)
{:error, {:renderer_serialization_failed, reason}}
end
end
defp safe_serialize_renderer(nil) do
{:ok, nil}
end
defp safe_serialize_renderer(invalid) do
Raxol.Core.Runtime.Log.error(
"Invalid renderer structure: #{inspect(invalid)}"
)
{:error, :invalid_renderer_structure}
end
defp safe_serialize_screen_buffer(%ScreenBuffer{} = buffer) do
with {:ok, cells} <- safe_serialize_cells(buffer.cells) do
{:ok,
%{
width: buffer.width,
height: buffer.height,
cells: cells,
cursor_position: buffer.cursor_position
}}
else
{:error, reason} ->
Raxol.Core.Runtime.Log.error(
"ScreenBuffer serialization failed: #{inspect(reason)}"
)
{:error, {:buffer_serialization_failed, reason}}
end
end
defp safe_serialize_screen_buffer(%ConsolidatedBuffer{} = buffer) do
with {:ok, cells} <- safe_serialize_cells(buffer.cells) do
{:ok,
%{
width: buffer.width,
height: buffer.height,
cells: cells,
cursor_position: buffer.cursor_position
}}
else
{:error, reason} ->
Raxol.Core.Runtime.Log.error(
"ConsolidatedBuffer serialization failed: #{inspect(reason)}"
)
{:error, {:buffer_serialization_failed, reason}}
end
end
defp safe_serialize_screen_buffer([]) do
{:ok, []}
end
defp safe_serialize_screen_buffer(buffers) when is_list(buffers) do
results = Enum.map(buffers, &safe_serialize_screen_buffer/1)
errors =
Enum.filter(results, fn
{:error, _} -> true
_ -> false
end)
case Enum.empty?(errors) do
true ->
serialized = Enum.map(results, fn {:ok, data} -> data end)
{:ok, serialized}
false ->
{:error, {:multiple_buffer_errors, errors}}
end
end
defp safe_serialize_screen_buffer(nil) do
{:ok, nil}
end
defp safe_serialize_screen_buffer(invalid) do
Raxol.Core.Runtime.Log.error(
"Invalid screen buffer structure: #{inspect(invalid)}"
)
{:error, :invalid_screen_buffer}
end
defp safe_serialize_cells(cells) when is_list(cells) do
result =
cells
|> Enum.with_index()
|> Enum.reduce_while({:ok, []}, fn {row, row_idx}, {:ok, acc} ->
case safe_serialize_row(row, row_idx) do
{:ok, serialized_row} ->
{:cont, {:ok, acc ++ [serialized_row]}}
{:error, reason} ->
{:halt, {:error, {:row_serialization_failed, row_idx, reason}}}
end
end)
case result do
{:ok, serialized} -> {:ok, serialized}
error -> error
end
end
defp safe_serialize_cells(nil) do
{:ok, []}
end
defp safe_serialize_row(row, _row_idx) when is_list(row) do
results = Enum.map(row, &safe_serialize_cell/1)
errors =
Enum.filter(results, fn
{:error, _} -> true
_ -> false
end)
case Enum.empty?(errors) do
true ->
serialized = Enum.map(results, fn {:ok, data} -> data end)
{:ok, serialized}
false ->
{:error, {:cell_errors, errors}}
end
end
defp safe_serialize_cell(%Raxol.Terminal.Cell{} = cell) do
with {:ok, style} <- safe_serialize_style(cell.style) do
{:ok,
%{
char: cell.char,
style: style,
dirty: cell.dirty,
wide_placeholder: cell.wide_placeholder
}}
else
{:error, reason} ->
Raxol.Core.Runtime.Log.error(
"Cell serialization failed: #{inspect(reason)}, cell: #{inspect(cell)}"
)
{:error, {:cell_serialization_failed, reason}}
end
end
defp safe_serialize_cell(nil) do
# Return default empty cell
{:ok,
%{
char: " ",
style: serialize_default_style(),
dirty: false,
wide_placeholder: false
}}
end
defp safe_serialize_cell(invalid) do
Raxol.Core.Runtime.Log.warning(
"Invalid cell structure: #{inspect(invalid)}"
)
# Return default cell instead of failing
safe_serialize_cell(nil)
end
defp safe_serialize_style(%Raxol.Terminal.ANSI.TextFormatting{} = style) do
serialize_style_struct(style)
end
defp safe_serialize_style(nil) do
{:ok, serialize_default_style()}
end
defp safe_serialize_style(style) do
Raxol.Core.Runtime.Log.warning(
"Unknown style type encountered during serialization: #{inspect(style)}"
)
{:error, {:unknown_style_type, style}}
end
defp serialize_style_struct(style) do
case Raxol.Core.ErrorHandling.safe_call(fn ->
{:ok, extract_style_fields(style)}
end) do
{:ok, result} ->
result
{:error, {error, _stacktrace}} ->
Raxol.Core.Runtime.Log.error(
"Style serialization exception: #{inspect(error)}"
)
{:error, {:style_serialization_exception, error}}
end
end
defp extract_style_fields(style) do
%{
bold: style.bold,
italic: style.italic,
underline: style.underline,
blink: style.blink,
reverse: style.reverse,
foreground: style.foreground,
background: style.background,
double_width: Map.get(style, :double_width, false),
double_height: Map.get(style, :double_height, false),
faint: Map.get(style, :faint, false),
conceal: Map.get(style, :conceal, false),
strikethrough: Map.get(style, :strikethrough, false),
fraktur: Map.get(style, :fraktur, false),
double_underline: Map.get(style, :double_underline, false),
framed: Map.get(style, :framed, false),
encircled: Map.get(style, :encircled, false),
overlined: Map.get(style, :overlined, false),
hyperlink: Map.get(style, :hyperlink)
}
end
defp serialize_default_style do
%{
bold: false,
italic: false,
underline: false,
blink: false,
reverse: false,
foreground: nil,
background: nil,
double_width: false,
double_height: false,
faint: false,
conceal: false,
strikethrough: false,
fraktur: false,
double_underline: false,
framed: false,
encircled: false,
overlined: false,
hyperlink: nil
}
end
# Deserialization functions
defp deserialize_emulator(emulator_data) when is_map(emulator_data) do
with {:ok, main_screen_buffer} <-
deserialize_screen_buffer(
Map.get(emulator_data, :main_screen_buffer)
),
{:ok, alternate_screen_buffer} <-
deserialize_screen_buffer(
Map.get(emulator_data, :alternate_screen_buffer)
),
{:ok, scrollback_buffer} <-
deserialize_screen_buffer(Map.get(emulator_data, :scrollback_buffer)) do
emulator = %Emulator{
main_screen_buffer: main_screen_buffer,
alternate_screen_buffer: alternate_screen_buffer,
active_buffer_type: Map.get(emulator_data, :active_buffer_type),
scrollback_buffer: scrollback_buffer,
cursor: Map.get(emulator_data, :cursor),
mode_manager: Map.get(emulator_data, :mode_manager),
style: Map.get(emulator_data, :style),
charset_state: Map.get(emulator_data, :charset_state),
width: Map.get(emulator_data, :width),
height: Map.get(emulator_data, :height),
window_state: Map.get(emulator_data, :window_state),
state_stack: Map.get(emulator_data, :state_stack, []),
output_buffer: Map.get(emulator_data, :output_buffer, ""),
scrollback_limit: Map.get(emulator_data, :scrollback_limit, 1000),
window_title: Map.get(emulator_data, :window_title),
plugin_manager: Map.get(emulator_data, :plugin_manager),
saved_cursor: Map.get(emulator_data, :saved_cursor),
scroll_region: Map.get(emulator_data, :scroll_region),
sixel_state: Map.get(emulator_data, :sixel_state),
last_col_exceeded: Map.get(emulator_data, :last_col_exceeded, false),
cursor_blink_rate: Map.get(emulator_data, :cursor_blink_rate),
cursor_style: Map.get(emulator_data, :cursor_style),
session_id: Map.get(emulator_data, :session_id),
client_options: Map.get(emulator_data, :client_options, %{})
}
{:ok, emulator}
else
{:error, reason} ->
{:error, {:emulator_deserialization_failed, reason}}
end
end
defp deserialize_emulator(nil) do
{:ok, nil}
end
defp deserialize_emulator(_invalid) do
{:error, :invalid_emulator_data}
end
defp deserialize_renderer(%{screen_buffer: buffer_data, theme: theme}) do
with {:ok, screen_buffer} <- deserialize_screen_buffer(buffer_data) do
renderer = %{
screen_buffer: screen_buffer,
theme: theme
}
{:ok, renderer}
else
{:error, reason} ->
{:error, {:renderer_deserialization_failed, reason}}
end
end
defp deserialize_renderer(nil) do
{:ok, nil}
end
defp deserialize_renderer(_invalid) do
{:error, :invalid_renderer_data}
end
defp deserialize_screen_buffer(%{
width: width,
height: height,
cells: cells,
cursor_position: cursor_position
}) do
with {:ok, deserialized_cells} <- safe_deserialize_cells(cells) do
screen_buffer = %ScreenBuffer{
width: width,
height: height,
cells: deserialized_cells,
cursor_position: cursor_position,
scrollback: [],
scrollback_limit: 1000,
selection: nil,
scroll_region: nil,
scroll_position: 0,
damage_regions: [],
default_style: Raxol.Terminal.ANSI.TextFormatting.Core.new()
}
{:ok, screen_buffer}
else
{:error, reason} ->
{:error, {:buffer_deserialization_failed, reason}}
end
end
defp deserialize_screen_buffer([]) do
{:ok, []}
end
defp deserialize_screen_buffer(buffers) when is_list(buffers) do
results = Enum.map(buffers, &deserialize_screen_buffer/1)
errors =
Enum.filter(results, fn
{:error, _} -> true
_ -> false
end)
case Enum.empty?(errors) do
true ->
deserialized = Enum.map(results, fn {:ok, data} -> data end)
{:ok, deserialized}
false ->
{:error, {:multiple_buffer_errors, errors}}
end
end
defp deserialize_screen_buffer(nil) do
{:ok, nil}
end
defp deserialize_screen_buffer(_invalid) do
{:error, :invalid_screen_buffer_data}
end
defp safe_deserialize_cells(cells) when is_list(cells) do
deserialized_rows =
cells
|> Enum.with_index()
|> Enum.map(fn {row, row_idx} ->
{:ok, deserialized_row} = safe_deserialize_row(row, row_idx)
deserialized_row
end)
{:ok, deserialized_rows}
end
defp safe_deserialize_cells(nil) do
{:ok, []}
end
defp safe_deserialize_row(row, _row_idx) when is_list(row) do
results = Enum.map(row, &safe_deserialize_cell/1)
errors =
Enum.filter(results, fn
{:error, _} -> true
_ -> false
end)
case Enum.empty?(errors) do
true ->
deserialized = Enum.map(results, fn {:ok, data} -> data end)
{:ok, deserialized}
false ->
# Log errors but continue with default cells
Raxol.Core.Runtime.Log.warning(
"Some cells failed to deserialize: #{inspect(errors)}"
)
deserialized =
Enum.map(results, fn
{:ok, data} -> data
{:error, _} -> create_default_cell()
end)
{:ok, deserialized}
end
end
defp safe_deserialize_cell(%{
char: char,
style: style_data,
dirty: dirty,
wide_placeholder: wide_placeholder
}) do
with {:ok, style} <- safe_deserialize_style(style_data) do
{:ok,
%Raxol.Terminal.Cell{
char: char,
style: style,
dirty: dirty,
wide_placeholder: wide_placeholder
}}
else
{:error, reason} ->
Raxol.Core.Runtime.Log.warning(
"Cell deserialization failed: #{inspect(reason)}, using default"
)
{:ok, create_default_cell()}
end
end
defp safe_deserialize_cell(_invalid) do
{:ok, create_default_cell()}
end
defp create_default_cell do
%Raxol.Terminal.Cell{
char: " ",
style: Raxol.Terminal.ANSI.TextFormatting.Core.new(),
dirty: false,
wide_placeholder: false
}
end
defp safe_deserialize_style(style_data) when is_map(style_data) do
case Raxol.Core.ErrorHandling.safe_call(fn ->
style = %Raxol.Terminal.ANSI.TextFormatting{
bold: Map.get(style_data, :bold, false),
italic: Map.get(style_data, :italic, false),
underline: Map.get(style_data, :underline, false),
blink: Map.get(style_data, :blink, false),
reverse: Map.get(style_data, :reverse, false),
foreground: Map.get(style_data, :foreground),
background: Map.get(style_data, :background),
double_width: Map.get(style_data, :double_width, false),
double_height: Map.get(style_data, :double_height, false),
faint: Map.get(style_data, :faint, false),
conceal: Map.get(style_data, :conceal, false),
strikethrough: Map.get(style_data, :strikethrough, false),
fraktur: Map.get(style_data, :fraktur, false),
double_underline: Map.get(style_data, :double_underline, false),
framed: Map.get(style_data, :framed, false),
encircled: Map.get(style_data, :encircled, false),
overlined: Map.get(style_data, :overlined, false),
hyperlink: Map.get(style_data, :hyperlink)
}
{:ok, style}
end) do
{:ok, result} ->
result
{:error, {error, _stacktrace}} ->
Raxol.Core.Runtime.Log.warning(
"Style deserialization exception: #{inspect(error)}"
)
{:ok, Raxol.Terminal.ANSI.TextFormatting.Core.new()}
end
end
defp safe_deserialize_style(nil) do
{:ok, Raxol.Terminal.ANSI.TextFormatting.Core.new()}
end
defp safe_deserialize_style(_invalid) do
{:ok, Raxol.Terminal.ANSI.TextFormatting.Core.new()}
end
end