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 modes handlers screen_buffer_handler.ex
Raw

lib/raxol/terminal/modes/handlers/screen_buffer_handler.ex

defmodule Raxol.Terminal.Modes.Handlers.ScreenBufferHandler do
@moduledoc """
Handles screen buffer mode operations and their side effects.
Manages alternate screen buffer switching and related functionality.
"""
require Raxol.Core.Runtime.Log
alias Raxol.Terminal.Emulator
alias Raxol.Terminal.ANSI.TextFormatting
alias Raxol.Terminal.Modes.Types.ModeTypes
alias Raxol.Terminal.ModeManager
@screen_buffer_module Application.compile_env(
:raxol,
:screen_buffer_impl,
Raxol.Terminal.ScreenBuffer
)
@doc """
Handles a screen buffer mode change and applies its effects to the emulator.
"""
@spec handle_mode_change(atom(), ModeTypes.mode_value(), Emulator.t()) ::
{:ok, Emulator.t()} | {:error, term()}
def handle_mode_change(mode_name, value, emulator) do
case find_mode_definition(mode_name) do
%{category: :screen_buffer} = mode_def ->
apply_mode_effects(mode_def, value, emulator)
_ ->
{:error, :invalid_mode}
end
end
# Private Functions
defp find_mode_definition(mode_name) do
ModeTypes.get_all_modes()
|> Map.values()
|> Enum.find(&(&1.name == mode_name))
end
defp apply_mode_effects(mode_def, value, emulator) do
case mode_def.name do
:dec_alt_screen ->
handle_simple_alt_screen(value, emulator)
:dec_alt_screen_save ->
handle_alt_screen_with_save(value, emulator)
:alt_screen_buffer ->
handle_alt_screen_with_clear(value, emulator)
:decsc_deccara ->
handle_cursor_save_restore(value, emulator)
_ ->
{:error, :unsupported_mode}
end
end
defp handle_simple_alt_screen(true, emulator) do
# Mode 47: Simple alternate screen buffer
with {:ok, alt_buffer} <- create_or_get_alt_buffer(emulator) do
{:ok,
%{
emulator
| alternate_screen_buffer: alt_buffer,
active_buffer_type: :alternate
}}
end
end
defp handle_simple_alt_screen(false, emulator) do
# Switch back to main buffer
{:ok, %{emulator | active_buffer_type: :main}}
end
defp handle_alt_screen_with_save(true, emulator) do
# Mode 1047: Alt screen with save/restore
with {:ok, alt_buffer} <- create_or_get_alt_buffer(emulator),
{:ok, emulator_with_saved_state} <- save_terminal_state(emulator) do
# Set alternate_buffer_active to true in mode_manager
new_mode_manager =
Map.put(
emulator_with_saved_state.mode_manager,
:alternate_buffer_active,
true
)
# Reset cursor position to (0, 0) when switching to alternate buffer
updated_cursor =
Raxol.Terminal.Cursor.Manager.set_position(
emulator_with_saved_state.cursor,
{0, 0}
)
{:ok,
%{
emulator_with_saved_state
| alternate_screen_buffer: alt_buffer,
active_buffer_type: :alternate,
mode_manager: new_mode_manager,
cursor: updated_cursor
}}
end
end
defp handle_alt_screen_with_save(false, emulator) do
# Switch back to main buffer and restore state
with {:ok, emulator_with_restored_state} <- restore_terminal_state(emulator) do
# Set alternate_buffer_active to false in mode_manager
new_mode_manager =
Map.put(
emulator_with_restored_state.mode_manager,
:alternate_buffer_active,
false
)
{:ok,
%{
emulator_with_restored_state
| active_buffer_type: :main,
mode_manager: new_mode_manager
}}
end
end
defp handle_alt_screen_with_clear(true, emulator) do
# Mode 1049: Alt screen with save/restore and clear
with {:ok, alt_buffer} <- create_or_get_alt_buffer(emulator),
{:ok, emulator_with_saved_state} <- save_terminal_state(emulator) do
# Clear the alternate buffer
cleared_alt_buffer =
@screen_buffer_module.clear(
alt_buffer,
TextFormatting.new()
)
# Create a fresh mode manager with default values for alternate screen
# but preserve certain critical modes that should persist
default_mode_manager = ModeManager.new()
updated_mode_manager = %{
default_mode_manager
| alternate_buffer_active: true,
# Preserve only critical modes that should persist across screen switches
interlacing_mode:
emulator_with_saved_state.mode_manager.interlacing_mode
}
{:ok,
%{
emulator_with_saved_state
| alternate_screen_buffer: cleared_alt_buffer,
active_buffer_type: :alternate,
mode_manager: updated_mode_manager
}}
end
end
defp handle_alt_screen_with_clear(false, emulator) do
# Switch back to main buffer, restore state, and clear alt buffer
with {:ok, emulator_with_restored_state} <- restore_terminal_state(emulator) do
# Clear the alternate buffer before switching away
case emulator_with_restored_state.alternate_screen_buffer do
nil ->
{:ok, %{emulator_with_restored_state | active_buffer_type: :main}}
alt_buf ->
cleared_alt_buf =
@screen_buffer_module.clear(
alt_buf,
TextFormatting.new()
)
{:ok,
%{
emulator_with_restored_state
| alternate_screen_buffer: cleared_alt_buf,
active_buffer_type: :main
}}
end
end
end
defp create_or_get_alt_buffer(emulator) do
case emulator.alternate_screen_buffer do
nil ->
{width, height} =
@screen_buffer_module.get_dimensions(emulator.main_screen_buffer)
{:ok, @screen_buffer_module.new(width, height)}
alt_buffer ->
{:ok, alt_buffer}
end
end
defp save_terminal_state(emulator) do
# Get the terminal state implementation
terminal_state_module =
Application.get_env(
:raxol,
:terminal_state_impl,
Raxol.Terminal.ANSI.TerminalState
)
# Save the current state
new_stack = terminal_state_module.save_state(emulator.state_stack, emulator)
{:ok, %{emulator | state_stack: new_stack}}
end
defp restore_terminal_state(emulator) do
# Get the terminal state implementation
terminal_state_module =
Application.get_env(
:raxol,
:terminal_state_impl,
Raxol.Terminal.ANSI.TerminalState
)
# Restore the previous state
{restored_state, new_stack} =
terminal_state_module.restore_state(emulator.state_stack)
case restored_state do
nil ->
{:ok, emulator}
state ->
# Apply the restored state
emulator_with_restored_state =
terminal_state_module.apply_restored_data(
emulator,
state,
[
:cursor,
:style,
:charset_state,
:mode_manager,
:scroll_region,
:cursor_style
]
)
{:ok, %{emulator_with_restored_state | state_stack: new_stack}}
end
end
defp handle_cursor_save_restore(true, emulator) do
# Mode 1048: Save cursor position and attributes
save_terminal_state(emulator)
end
defp handle_cursor_save_restore(false, emulator) do
# Mode 1048: Restore cursor position and attributes only
restore_cursor_only(emulator)
end
defp restore_cursor_only(emulator) do
# Get the terminal state implementation
terminal_state_module =
Application.get_env(
:raxol,
:terminal_state_impl,
Raxol.Terminal.ANSI.TerminalState
)
# Restore the previous state
{restored_state, new_stack} =
terminal_state_module.restore_state(emulator.state_stack)
# Only restore the cursor position
emulator =
terminal_state_module.apply_restored_data(emulator, restored_state, [
:cursor
])
{:ok, %{emulator | state_stack: new_stack}}
end
end