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 terminal_server.ex
Raw

lib/raxol/terminal/terminal_server.ex

defmodule Raxol.Terminal.TerminalServer do
@moduledoc """
Unified terminal management system that consolidates all terminal operations.
This module serves as the central coordination point for all terminal functionality,
replacing the fragmented manager pattern with a single, cohesive system that handles:
- Terminal emulation and state management
- Command processing and routing
- Session management and lifecycle
- Buffer operations and memory management
- Event handling and notifications
- Configuration and preferences
- Plugin integration and extensions
- Performance monitoring and optimization
## Design Principles
1. **Single Responsibility**: Each operation has a clear, focused purpose
2. **Unified Interface**: All terminal operations go through this manager
3. **State Consistency**: Centralized state management prevents conflicts
4. **Performance**: Optimized for high-throughput terminal operations
5. **Extensibility**: Plugin system for custom functionality
6. **Reliability**: Comprehensive error handling and recovery
## Architecture Overview
```
UnifiedManager
├── Command Processing (CSIHandler)
├── Session Management (SessionManager)
├── Buffer Management (AdvancedManager)
├── State Management (StateManager)
├── Event System (EventManager)
├── Plugin System (PluginManager)
└── Performance Monitor (MetricsManager)
```
## Usage
# Start the unified manager
{:ok, manager} = UnifiedManager.start_link()
# Create a terminal session
{:ok, session} = UnifiedManager.create_session(manager, user_id, config)
# Process terminal input
{:ok, updated_session} = UnifiedManager.process_input(manager, session_id, input)
# Get terminal output
{:ok, output} = UnifiedManager.get_output(manager, session_id)
# Handle terminal commands
{:ok, session} = UnifiedManager.execute_command(manager, session_id, command)
"""
use Raxol.Core.Behaviours.BaseManager
alias Raxol.Core.Events.EventManager
alias Raxol.Core.Runtime.Log
alias Raxol.Core.Utils.TimerManager
alias Raxol.Terminal.Emulator
alias Raxol.Terminal.ScreenBuffer.Manager, as: BufferManager
defstruct [
:sessions,
:command_handler,
:buffer_manager,
:state_manager,
:event_manager,
:config,
:metrics,
:plugins
]
@type session_id :: String.t()
@type user_id :: String.t()
@type terminal_input :: String.t() | list(integer())
@type terminal_output :: String.t()
@type session_config :: map()
## Public API
@doc """
Creates a new terminal session.
"""
@spec create_session(GenServer.server(), user_id(), session_config()) ::
{:ok, session_id()} | {:error, term()}
def create_session(manager \\ __MODULE__, user_id, config \\ %{}) do
GenServer.call(manager, {:create_session, user_id, config})
end
@doc """
Terminates a terminal session.
"""
@spec terminate_session(GenServer.server(), session_id()) ::
:ok | {:error, term()}
def terminate_session(manager \\ __MODULE__, session_id) do
GenServer.call(manager, {:terminate_session, session_id})
end
@doc """
Processes terminal input (keyboard input, paste, etc.).
"""
@spec process_input(GenServer.server(), session_id(), terminal_input()) ::
{:ok, terminal_output()} | {:error, term()}
def process_input(manager \\ __MODULE__, session_id, input) do
GenServer.call(manager, {:process_input, session_id, input})
end
@doc """
Gets the current terminal output for rendering.
"""
@spec get_output(GenServer.server(), session_id()) ::
{:ok, terminal_output()} | {:error, term()}
def get_output(manager \\ __MODULE__, session_id) do
GenServer.call(manager, {:get_output, session_id})
end
@doc """
Executes a specific terminal command.
"""
@spec execute_command(GenServer.server(), session_id(), term()) ::
{:ok, term()} | {:error, term()}
def execute_command(manager \\ __MODULE__, session_id, command) do
GenServer.call(manager, {:execute_command, session_id, command})
end
@doc """
Resizes a terminal session.
"""
@spec resize_session(
GenServer.server(),
session_id(),
pos_integer(),
pos_integer()
) ::
:ok | {:error, term()}
def resize_session(manager \\ __MODULE__, session_id, width, height) do
GenServer.call(manager, {:resize_session, session_id, width, height})
end
@doc """
Gets session information and statistics.
"""
@spec get_session_info(GenServer.server(), session_id()) ::
{:ok, map()} | {:error, term()}
def get_session_info(manager \\ __MODULE__, session_id) do
GenServer.call(manager, {:get_session_info, session_id})
end
@doc """
Lists all active sessions.
"""
@spec list_sessions(GenServer.server()) :: {:ok, list(map())}
def list_sessions(manager \\ __MODULE__) do
GenServer.call(manager, :list_sessions)
end
@doc """
Gets overall manager statistics and health information.
"""
@spec get_manager_stats(GenServer.server()) :: {:ok, map()}
def get_manager_stats(manager \\ __MODULE__) do
GenServer.call(manager, :get_manager_stats)
end
@doc """
Performs cleanup operations (expired sessions, memory optimization).
"""
@spec cleanup(GenServer.server()) :: :ok
def cleanup(manager \\ __MODULE__) do
GenServer.call(manager, :cleanup)
end
## GenServer Implementation
@impl true
def init_manager(config) do
# Initialize subsystems
# BufferManager is a functional module, not a GenServer
{:ok, state_manager} = Raxol.Core.StateManager.start_link([])
{:ok, event_manager} = EventManager.start_link(name: EventManager)
# Initialize state
state = %__MODULE__{
sessions: %{},
command_handler: Raxol.Terminal.Commands.CSIHandler,
# Module reference, not a pid
buffer_manager: BufferManager,
state_manager: state_manager,
event_manager: event_manager,
config: config,
metrics: init_metrics(),
plugins: init_plugins(config.plugin_system_enabled)
}
# Schedule cleanup
_ = schedule_cleanup(config.cleanup_interval)
Log.info("Unified terminal manager initialized")
{:ok, state}
end
@impl Raxol.Core.Behaviours.BaseManager
def handle_manager_call({:create_session, user_id, config}, _from, state) do
case create_session_impl(state, user_id, config) do
{:ok, session_id, updated_state} ->
Log.info("Created terminal session #{session_id} for user #{user_id}")
{:reply, {:ok, session_id}, updated_state}
{:error, reason} ->
{:reply, {:error, reason}, state}
end
end
def handle_manager_call({:terminate_session, session_id}, _from, state) do
case terminate_session_impl(state, session_id) do
{:ok, updated_state} ->
Log.info("Terminated terminal session #{session_id}")
{:reply, :ok, updated_state}
{:error, reason} ->
{:reply, {:error, reason}, state}
end
end
def handle_manager_call({:process_input, session_id, input}, _from, state) do
case process_input_impl(state, session_id, input) do
{:ok, output, updated_state} ->
{:reply, {:ok, output}, updated_state}
{:error, reason} ->
{:reply, {:error, reason}, state}
end
end
def handle_manager_call({:get_output, session_id}, _from, state) do
case get_output_impl(state, session_id) do
{:ok, output} ->
{:reply, {:ok, output}, state}
{:error, reason} ->
{:reply, {:error, reason}, state}
end
end
def handle_manager_call({:execute_command, session_id, command}, _from, state) do
case execute_command_impl(state, session_id, command) do
{:ok, result, updated_state} ->
{:reply, {:ok, result}, updated_state}
{:error, reason} ->
{:reply, {:error, reason}, state}
end
end
def handle_manager_call(
{:resize_session, session_id, width, height},
_from,
state
) do
case resize_session_impl(state, session_id, width, height) do
{:ok, updated_state} ->
{:reply, :ok, updated_state}
{:error, reason} ->
{:reply, {:error, reason}, state}
end
end
def handle_manager_call({:get_session_info, session_id}, _from, state) do
case get_session_info_impl(state, session_id) do
{:ok, info} ->
{:reply, {:ok, info}, state}
{:error, reason} ->
{:reply, {:error, reason}, state}
end
end
def handle_manager_call(:list_sessions, _from, state) do
sessions = list_sessions_impl(state)
{:reply, {:ok, sessions}, state}
end
def handle_manager_call(:get_manager_stats, _from, state) do
stats = get_manager_stats_impl(state)
{:reply, {:ok, stats}, state}
end
def handle_manager_call(:cleanup, _from, state) do
updated_state = cleanup_impl(state)
{:reply, :ok, updated_state}
end
@impl Raxol.Core.Behaviours.BaseManager
def handle_manager_info(:cleanup_timer, state) do
updated_state = cleanup_impl(state)
_ = schedule_cleanup(state.config.cleanup_interval)
{:noreply, updated_state}
end
def handle_manager_info(_msg, state) do
{:noreply, state}
end
## Implementation Functions
defp create_session_impl(state, user_id, config) do
# Check session limit
case map_size(state.sessions) >= state.config.max_sessions do
true ->
{:error, :max_sessions_reached}
false ->
session_id = generate_session_id()
# Create emulator with configuration
width = Map.get(config, :width, state.config.default_width)
height = Map.get(config, :height, state.config.default_height)
emulator = Emulator.new(width, height)
session = %{
id: session_id,
user_id: user_id,
emulator: emulator,
created_at: DateTime.utc_now(),
last_activity: DateTime.utc_now(),
config: %{
width: width,
height: height,
scrollback_lines:
Map.get(
config,
:scrollback_lines,
state.config.default_scrollback
)
}
}
updated_sessions = Map.put(state.sessions, session_id, session)
updated_state = %{state | sessions: updated_sessions}
# Notify event system
EventManager.notify(state.event_manager, :session_created, %{
session_id: session_id,
user_id: user_id
})
{:ok, session_id, updated_state}
end
end
defp terminate_session_impl(state, session_id) do
case Map.get(state.sessions, session_id) do
nil ->
{:error, :session_not_found}
session ->
# Clean up emulator resources
cleanup_emulator(session.emulator)
# Remove from sessions
updated_sessions = Map.delete(state.sessions, session_id)
updated_state = %{state | sessions: updated_sessions}
# Notify event system
EventManager.notify(state.event_manager, :session_terminated, %{
session_id: session_id
})
{:ok, updated_state}
end
end
defp process_input_impl(state, session_id, input) do
case Map.get(state.sessions, session_id) do
nil ->
{:error, :session_not_found}
session ->
# Process input through emulator
{:ok, updated_emulator, output} =
process_emulator_input(session.emulator, input)
# Update session
updated_session = %{
session
| emulator: updated_emulator,
last_activity: DateTime.utc_now()
}
updated_sessions =
Map.put(state.sessions, session_id, updated_session)
updated_state = %{state | sessions: updated_sessions}
{:ok, output, updated_state}
end
end
defp get_output_impl(state, session_id) do
case Map.get(state.sessions, session_id) do
nil ->
{:error, :session_not_found}
session ->
output = extract_emulator_output(session.emulator)
{:ok, output}
end
end
defp execute_command_impl(state, session_id, command) do
case Map.get(state.sessions, session_id) do
nil ->
{:error, :session_not_found}
session ->
# Execute command through unified command handler
case Raxol.Terminal.Commands.CSIHandler.handle_csi_sequence(
session.emulator,
command.command,
command.params || []
) do
{:ok, updated_emulator} ->
updated_session = %{
session
| emulator: updated_emulator,
last_activity: DateTime.utc_now()
}
updated_sessions =
Map.put(state.sessions, session_id, updated_session)
updated_state = %{state | sessions: updated_sessions}
{:ok, :command_executed, updated_state}
{:error, reason, _emulator} ->
{:error, reason}
end
end
end
defp resize_session_impl(state, session_id, width, height) do
case Map.get(state.sessions, session_id) do
nil ->
{:error, :session_not_found}
session ->
# Resize emulator
updated_emulator = Emulator.resize(session.emulator, width, height)
updated_session = %{
session
| emulator: updated_emulator,
config: Map.merge(session.config, %{width: width, height: height}),
last_activity: DateTime.utc_now()
}
updated_sessions = Map.put(state.sessions, session_id, updated_session)
updated_state = %{state | sessions: updated_sessions}
# Notify event system
EventManager.notify(state.event_manager, :session_resized, %{
session_id: session_id,
width: width,
height: height
})
{:ok, updated_state}
end
end
defp get_session_info_impl(state, session_id) do
case Map.get(state.sessions, session_id) do
nil ->
{:error, :session_not_found}
session ->
info = %{
id: session.id,
user_id: session.user_id,
created_at: session.created_at,
last_activity: session.last_activity,
config: session.config,
emulator_state: get_emulator_summary(session.emulator)
}
{:ok, info}
end
end
defp list_sessions_impl(state) do
state.sessions
|> Enum.map(fn {_id, session} ->
%{
id: session.id,
user_id: session.user_id,
created_at: session.created_at,
last_activity: session.last_activity,
width: session.config.width,
height: session.config.height
}
end)
end
defp get_manager_stats_impl(state) do
%{
total_sessions: map_size(state.sessions),
active_sessions: count_active_sessions(state.sessions),
memory_usage: get_memory_usage(state),
uptime: get_uptime(),
config: state.config,
subsystem_stats: %{
buffer_manager: get_buffer_manager_stats(state.buffer_manager),
state_manager: get_state_manager_stats(state.state_manager),
event_manager: get_event_manager_stats(state.event_manager)
}
}
end
defp cleanup_impl(state) do
Log.debug("Running unified manager cleanup")
# Clean up inactive sessions
# 1 hour
cutoff_time = DateTime.add(DateTime.utc_now(), -3600, :second)
{active_sessions, inactive_sessions} =
Enum.split_with(state.sessions, fn {_id, session} ->
DateTime.compare(session.last_activity, cutoff_time) == :gt
end)
# Clean up inactive sessions
Enum.each(inactive_sessions, fn {_id, session} ->
cleanup_emulator(session.emulator)
end)
active_sessions_map = Map.new(active_sessions)
# Trigger subsystem cleanup
# BufferManager is a functional module, no cleanup needed
# StateManager cleanup is handled automatically
updated_state = %{state | sessions: active_sessions_map}
Log.info("Cleaned up #{length(inactive_sessions)} inactive sessions")
updated_state
end
## Helper Functions
defp generate_session_id do
:crypto.strong_rand_bytes(16)
|> Base.encode16(case: :lower)
end
defp schedule_cleanup(interval) when is_integer(interval) do
TimerManager.send_after(:cleanup_timer, interval)
end
defp schedule_cleanup(_), do: :ok
defp init_metrics do
%{
sessions_created: 0,
sessions_terminated: 0,
commands_processed: 0,
bytes_processed: 0,
start_time: System.monotonic_time()
}
end
defp init_plugins(false), do: %{enabled: false}
defp init_plugins(true) do
%{
enabled: true,
loaded_plugins: [],
plugin_registry: %{}
}
end
defp process_emulator_input(emulator, input) do
# Process input through the emulator
case Emulator.process_input(emulator, input) do
{updated_emulator, output} ->
{:ok, updated_emulator, output}
end
end
defp extract_emulator_output(emulator) do
Emulator.render_screen(emulator)
end
defp cleanup_emulator(emulator) do
# Clean up any emulator-specific resources
Emulator.cleanup(emulator)
end
defp get_emulator_summary(emulator) do
%{
width: emulator.width,
height: emulator.height,
cursor_position: Emulator.get_cursor_position(emulator),
screen_mode: emulator.screen_mode || :normal
}
end
defp count_active_sessions(sessions) do
# 5 minutes
cutoff_time = DateTime.add(DateTime.utc_now(), -300, :second)
Enum.count(sessions, fn {_id, session} ->
DateTime.compare(session.last_activity, cutoff_time) == :gt
end)
end
defp get_memory_usage(_state) do
# Get memory usage statistics
:erlang.memory()
end
defp get_uptime do
System.monotonic_time() - System.monotonic_time(:second)
end
defp get_buffer_manager_stats(buffer_manager) when is_pid(buffer_manager) do
case Process.alive?(buffer_manager) do
true -> GenServer.call(buffer_manager, :get_stats)
false -> %{status: :not_running}
end
rescue
e ->
Log.warning("Failed to get buffer manager stats: #{Exception.message(e)}")
%{status: :error}
end
defp get_buffer_manager_stats(_buffer_manager) do
%{status: :functional_module}
end
defp get_state_manager_stats(state_manager) do
case Process.alive?(state_manager) do
true -> GenServer.call(state_manager, :get_stats)
false -> %{status: :not_running}
end
rescue
e ->
Log.warning("Failed to get state manager stats: #{Exception.message(e)}")
%{status: :error}
end
defp get_event_manager_stats(event_manager) do
case Process.alive?(event_manager) do
true -> GenServer.call(event_manager, :get_stats)
false -> %{status: :not_running}
end
rescue
e ->
Log.warning("Failed to get event manager stats: #{Exception.message(e)}")
%{status: :error}
end
end