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 emulator struct.ex
Raw

lib/raxol/terminal/emulator/struct.ex

defmodule Raxol.Terminal.Emulator.Struct do
@moduledoc """
Provides terminal emulator structure and related functionality.
"""
alias Raxol.Terminal.ScreenBuffer
# Helper functions for cursor operations with pattern matching
defp call_cursor_operation(cursor, operation) when is_pid(cursor) do
GenServer.call(cursor, operation)
cursor
end
defp call_cursor_operation(cursor, {:move_to, col, row, width, height})
when is_map(cursor) do
Raxol.Terminal.Cursor.Manager.move_to(cursor, row, col, width, height)
end
defp call_cursor_operation(cursor, {:move_up, lines, width, height})
when is_map(cursor) do
Raxol.Terminal.Cursor.Manager.move_up(cursor, lines, width, height)
end
defp call_cursor_operation(cursor, {:move_down, lines, width, height})
when is_map(cursor) do
Raxol.Terminal.Cursor.Manager.move_down(cursor, lines, width, height)
end
defp call_cursor_operation(cursor, {:move_right, cols, width, height})
when is_map(cursor) do
Raxol.Terminal.Cursor.Manager.move_right(cursor, cols, width, height)
end
defp call_cursor_operation(cursor, {:move_left, cols, width, height})
when is_map(cursor) do
Raxol.Terminal.Cursor.Manager.move_left(cursor, cols, width, height)
end
defp call_cursor_operation(cursor, :move_to_line_start) when is_map(cursor) do
Raxol.Terminal.Cursor.Manager.move_to_line_start(cursor)
end
defp call_cursor_operation(cursor, {:move_to_column, column, width, height})
when is_map(cursor) do
Raxol.Terminal.Cursor.Manager.move_to_column(cursor, column, width, height)
end
defstruct [
:active_buffer,
:active_buffer_type,
:alternate_screen_buffer,
:charset_state,
:client_options,
:color_palette,
:command,
:command_history,
:current_command_buffer,
:current_hyperlink_url,
:cursor,
:cursor_manager,
:cursor_style,
:height,
:icon_name,
:last_col_exceeded,
:last_key_event,
:main_screen_buffer,
:max_command_history,
:memory_limit,
:mode_manager,
:output_buffer,
:parser_state,
:plugin_manager,
:saved_cursor,
:scroll_region,
:scrollback_buffer,
:scrollback_limit,
:session_id,
:state,
:state_stack,
:style,
:tab_stops,
:width,
:window_manager,
:window_title,
:current_hyperlink
]
@type t :: %__MODULE__{
active_buffer_type: :main | :alternate,
active_buffer: ScreenBuffer.t(),
scrollback_buffer: [ScreenBuffer.t()],
cursor_manager: term(),
mode_manager: term(),
command_history: [String.t()],
current_command_buffer: String.t(),
style: map(),
color_palette: map(),
tab_stops: [integer()],
cursor: %{
position: {integer(), integer()},
style: atom(),
visible: boolean(),
blink_state: boolean()
},
cursor_style: atom(),
saved_cursor:
%{
position: {integer(), integer()},
style: atom(),
visible: boolean(),
blink_state: boolean()
}
| nil,
charset_state: %{
g0: atom(),
g1: atom(),
g2: atom(),
g3: atom(),
gl: atom(),
gr: atom(),
single_shift: atom() | nil
},
width: non_neg_integer(),
height: non_neg_integer(),
main_screen_buffer: ScreenBuffer.t(),
alternate_screen_buffer: ScreenBuffer.t(),
scrollback_limit: non_neg_integer(),
memory_limit: non_neg_integer(),
max_command_history: non_neg_integer(),
plugin_manager: term(),
session_id: String.t(),
client_options: map(),
state: atom(),
window_manager: term(),
command: term(),
window_title: String.t() | nil,
state_stack: list(),
last_col_exceeded: boolean(),
icon_name: String.t() | nil,
current_hyperlink_url: String.t() | nil,
scroll_region: {non_neg_integer(), non_neg_integer()} | nil,
last_key_event: term(),
output_buffer: String.t(),
parser_state: term()
}
@doc """
Creates a new terminal emulator with the given options.
"""
@spec new(non_neg_integer(), non_neg_integer(), keyword()) :: t()
def new(width, height, opts \\ []) do
active_buffer = ScreenBuffer.new(width, height)
scrollback_buffer = []
initialize_emulator(
active_buffer,
scrollback_buffer,
Keyword.put(opts, :width, width)
)
end
@doc """
Gets the active buffer from the emulator.
"""
@spec get_screen_buffer(t()) :: ScreenBuffer.t()
def get_screen_buffer(emulator) do
case emulator.active_buffer_type do
:main -> emulator.main_screen_buffer
:alternate -> emulator.alternate_screen_buffer
_ -> emulator.main_screen_buffer
end
end
@doc """
Checks if scrolling is needed and performs it if necessary.
"""
@spec maybe_scroll(t()) :: t()
def maybe_scroll(emulator) do
case needs_scroll?(emulator) do
true -> perform_scroll(emulator)
false -> emulator
end
end
@doc """
Gets the cursor position from the emulator.
"""
@spec get_cursor_position(t()) :: {non_neg_integer(), non_neg_integer()}
def get_cursor_position(emulator) do
{emulator.cursor.row, emulator.cursor.col}
end
@doc """
Processes input for the emulator.
"""
@spec process_input(t(), String.t()) :: {t(), String.t()}
def process_input(emulator, input) do
# For now, just return the emulator unchanged and the input as output
# This is a placeholder implementation
{emulator, input}
end
@doc """
Sets a terminal mode.
"""
@spec set_mode(t(), atom()) :: t()
def set_mode(emulator, mode) do
case mode do
:show_cursor ->
%{emulator | cursor: %{emulator.cursor | visible: true}}
:insert_mode ->
%{emulator | state: :insert}
:irm ->
%{emulator | state: :insert}
_ ->
# For other modes, just return the emulator unchanged
emulator
end
end
@doc """
Resets a terminal mode.
"""
@spec reset_mode(t(), atom()) :: t()
def reset_mode(emulator, mode) do
case mode do
:show_cursor ->
%{emulator | cursor: %{emulator.cursor | visible: false}}
:insert_mode ->
%{emulator | state: :normal}
:irm ->
%{emulator | state: :normal}
_ ->
# For other modes, just return the emulator unchanged
emulator
end
end
@doc """
Sets the character set for the emulator.
"""
@spec set_charset(t(), atom()) :: {:ok, t()} | {:error, atom(), t()}
def set_charset(emulator, _charset) do
# For now, just return success
# This is a placeholder implementation
{:ok, emulator}
end
@doc """
Updates the active buffer in the emulator.
"""
@spec update_active_buffer(t(), ScreenBuffer.t()) :: t()
def update_active_buffer(emulator, new_buffer) do
case emulator.active_buffer_type do
:main ->
%{emulator | main_screen_buffer: new_buffer}
:alternate ->
%{emulator | alternate_screen_buffer: new_buffer}
_ ->
%{emulator | main_screen_buffer: new_buffer}
end
end
@doc """
Moves the cursor to the specified position.
"""
@spec move_cursor(t(), integer(), integer()) :: t()
def move_cursor(emulator, row, col) do
row = max(0, min(row, emulator.height - 1))
col = max(0, min(col, emulator.width - 1))
moved_cursor =
call_cursor_operation(
emulator.cursor,
{:move_to, col, row, emulator.width, emulator.height}
)
%{emulator | cursor: moved_cursor}
end
@doc """
Moves the cursor up by the specified number of lines.
"""
@spec move_cursor_up(t(), integer(), integer(), integer()) :: t()
def move_cursor_up(emulator, lines, _width, _height) do
moved_cursor =
call_cursor_operation(
emulator.cursor,
{:move_up, lines, emulator.width, emulator.height}
)
%{emulator | cursor: moved_cursor}
end
@doc """
Moves the cursor down by the specified number of lines.
"""
@spec move_cursor_down(t(), integer(), integer(), integer()) :: t()
def move_cursor_down(emulator, lines, _width, _height) do
moved_cursor =
call_cursor_operation(
emulator.cursor,
{:move_down, lines, emulator.width, emulator.height}
)
%{emulator | cursor: moved_cursor}
end
@doc """
Moves the cursor right by the specified number of columns.
"""
@spec move_cursor_right(t(), integer(), integer(), integer()) :: t()
def move_cursor_right(emulator, cols, _width, _height) do
moved_cursor =
call_cursor_operation(
emulator.cursor,
{:move_right, cols, emulator.width, emulator.height}
)
%{emulator | cursor: moved_cursor}
end
@doc """
Moves the cursor left by the specified number of columns.
"""
@spec move_cursor_left(t(), integer(), integer(), integer()) :: t()
def move_cursor_left(emulator, cols, _width, _height) do
moved_cursor =
call_cursor_operation(
emulator.cursor,
{:move_left, cols, emulator.width, emulator.height}
)
%{emulator | cursor: moved_cursor}
end
@doc """
Moves the cursor to the start of the current line.
"""
@spec move_cursor_to_line_start(t()) :: t()
def move_cursor_to_line_start(emulator) do
moved_cursor =
call_cursor_operation(
emulator.cursor,
:move_to_line_start
)
%{emulator | cursor: moved_cursor}
end
@doc """
Moves the cursor to the specified column.
"""
@spec move_cursor_to_column(t(), integer(), integer(), integer()) :: t()
def move_cursor_to_column(emulator, column, _width, _height) do
moved_cursor =
call_cursor_operation(
emulator.cursor,
{:move_to_column, column, emulator.width, emulator.height}
)
%{emulator | cursor: moved_cursor}
end
@doc """
Moves the cursor to the specified position.
"""
@spec move_cursor_to(t(), {integer(), integer()}, integer(), integer()) :: t()
def move_cursor_to(emulator, {row, col}, _width, _height) do
moved_cursor =
call_cursor_operation(
emulator.cursor,
{:move_to, col, row, emulator.width, emulator.height}
)
%{emulator | cursor: moved_cursor}
end
# Private helper functions
defp needs_scroll?(emulator) do
emulator.cursor.position |> elem(1) >= emulator.height
end
defp perform_scroll(emulator) do
# Implementation
emulator
end
defp default_opts(opts) do
%{
width: Keyword.get(opts, :width, 80),
height: Keyword.get(opts, :height, 24),
scrollback_limit: Keyword.get(opts, :scrollback_limit, 1000),
memory_limit: Keyword.get(opts, :memory_limit, 100_000),
max_command_history: Keyword.get(opts, :max_command_history, 100),
plugin_manager: Keyword.get(opts, :plugin_manager),
session_id: Keyword.get(opts, :session_id, Ecto.UUID.generate()),
client_options: Keyword.get(opts, :client_options, %{}),
state: Keyword.get(opts, :state, :normal),
window_manager: Keyword.get(opts, :window_manager)
}
end
defp initialize_emulator(active_buffer, scrollback_buffer, opts) do
defaults = default_opts(opts)
%__MODULE__{
active_buffer: active_buffer,
active_buffer_type: :main,
scrollback_buffer: scrollback_buffer,
cursor_manager: Keyword.get(opts, :cursor_manager),
mode_manager: Keyword.get(opts, :mode_manager),
command_history: [],
current_command_buffer: "",
style: %{},
color_palette: %{},
tab_stops: [],
cursor: Raxol.Terminal.Cursor.Manager.new(),
cursor_style: :block,
saved_cursor: nil,
charset_state: %{
g0: :us_ascii,
g1: :us_ascii,
g2: :us_ascii,
g3: :us_ascii,
gl: :g0,
gr: :g1,
single_shift: nil
},
width: defaults.width,
height: defaults.height,
main_screen_buffer: active_buffer,
alternate_screen_buffer:
ScreenBuffer.new(defaults.width, defaults.height),
scrollback_limit: defaults.scrollback_limit,
memory_limit: defaults.memory_limit,
max_command_history: defaults.max_command_history,
plugin_manager: defaults.plugin_manager,
session_id: defaults.session_id,
client_options: defaults.client_options,
state: defaults.state,
window_manager: defaults.window_manager,
output_buffer: "",
parser_state: %{state: :ground}
}
end
end