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
raxol_terminal lib raxol terminal rendering render_server.ex
Raw

lib/raxol/terminal/rendering/render_server.ex

defmodule Raxol.Terminal.Rendering.RenderServer do
@moduledoc """
Provides a unified interface for terminal rendering operations.
"""
use Raxol.Core.Behaviours.BaseManager
alias Raxol.Terminal.Buffer
alias Raxol.Terminal.Env
defstruct [
:buffer,
:screen,
:style,
:cursor_visible,
:title,
:termbox_initialized,
:fps,
:theme,
:font_settings,
:cache
]
@type t :: %__MODULE__{
buffer: Buffer.t(),
screen: term(),
style: term(),
cursor_visible: boolean(),
title: String.t(),
termbox_initialized: boolean(),
fps: integer(),
theme: map(),
font_settings: map(),
cache: map()
}
# Client API
@doc """
Renders the current state.
"""
@spec render(t()) :: :ok
def render(state) do
GenServer.call(__MODULE__, {:render, state})
end
@doc """
Renders the current state with a specific renderer ID.
"""
@spec render(t(), String.t()) :: :ok
def render(state, _renderer_id) do
GenServer.call(__MODULE__, {:render, state})
end
@doc """
Updates the renderer configuration.
"""
@spec update_config(t(), map()) :: :ok
def update_config(state, config) do
GenServer.call(__MODULE__, {:update_config, state, config})
end
@doc """
Updates the renderer configuration with a single argument.
"""
@spec update_config(map()) :: :ok
def update_config(config) do
GenServer.call(__MODULE__, {:update_config, nil, config})
end
@doc """
Cleans up resources.
"""
@spec cleanup(t()) :: :ok
def cleanup(%__MODULE__{} = state) do
GenServer.call(__MODULE__, {:cleanup, state})
end
def cleanup(_other) do
# Handle cases where a plain map or other type is passed
:ok
end
@doc """
Resizes the renderer.
"""
@spec resize(non_neg_integer(), non_neg_integer()) :: :ok
def resize(width, height) do
GenServer.call(__MODULE__, {:resize, width, height})
end
@doc """
Sets cursor visibility.
"""
@spec set_cursor_visibility(boolean()) :: :ok
def set_cursor_visibility(visible) do
GenServer.call(__MODULE__, {:set_cursor_visibility, visible})
end
@doc """
Sets the window title.
"""
@spec set_title(String.t()) :: :ok
def set_title(title) do
GenServer.call(__MODULE__, {:set_title, title})
end
@doc """
Gets the current window title.
"""
@spec get_title() :: String.t()
def get_title do
GenServer.call(__MODULE__, :get_title)
end
@doc """
Initializes the terminal.
"""
@spec init_terminal() :: :ok
def init_terminal do
GenServer.call(__MODULE__, :init_terminal)
end
@doc """
Shuts down the terminal.
"""
@spec shutdown_terminal() :: :ok
def shutdown_terminal do
case Process.whereis(__MODULE__) do
nil -> :ok
_ -> GenServer.call(__MODULE__, :shutdown_terminal)
end
end
@doc """
Sets a specific configuration value.
"""
@spec set_config_value(atom(), any()) :: :ok
def set_config_value(key, value) do
GenServer.call(__MODULE__, {:set_config_value, key, value})
end
@doc """
Resets the configuration to defaults.
"""
@spec reset_config() :: :ok
def reset_config do
GenServer.call(__MODULE__, :reset_config)
end
# Server Callbacks
@doc """
Initializes the GenServer with default state.
"""
@impl Raxol.Core.Behaviours.BaseManager
def init_manager(opts) do
initial_state = build_initial_state(opts)
{:ok, initial_state}
end
defp build_initial_state(opts) do
%__MODULE__{
buffer: get_opt(opts, :buffer, &Buffer.new/0),
screen: get_opt(opts, :screen, %{}),
style: get_opt(opts, :style, %{}),
cursor_visible: get_opt(opts, :cursor_visible, true),
title: get_opt(opts, :title, ""),
termbox_initialized: false,
fps: get_opt(opts, :fps, 60),
theme: get_opt(opts, :theme, %{}),
font_settings: get_opt(opts, :font_settings, %{}),
cache: get_opt(opts, :cache, %{})
}
end
defp get_opt(opts, key, default) when is_function(default, 0) do
opts[key] || default.()
end
defp get_opt(opts, key, default) do
opts[key] || default
end
@impl true
def handle_call({:render, state}, _from, renderer) do
handle_render_request(renderer.termbox_initialized, state, renderer)
end
@impl true
def handle_call({:update_config, _state, config}, _from, renderer) do
new_state = %{
renderer
| fps: Map.get(config, :fps, renderer.fps),
theme: Map.get(config, :theme, renderer.theme),
font_settings: Map.get(config, :font_settings, renderer.font_settings),
style: Map.merge(renderer.style, Map.get(config, :style, %{})),
cursor_visible: Map.get(config, :cursor_visible, renderer.cursor_visible)
}
{:reply, :ok, new_state}
end
def handle_call({:cleanup, _state}, _from, renderer) do
# Cleanup termbox only if not in test mode
cleanup_termbox()
{:reply, :ok, renderer}
end
def handle_call({:resize, width, height}, _from, renderer) do
# Resize termbox only if not in test mode
resize_termbox(width, height)
# Update screen buffer with new dimensions
new_screen = Map.put(renderer.screen || %{}, :width, width)
new_screen = Map.put(new_screen, :height, height)
{:reply, :ok, %{renderer | screen: new_screen}}
end
def handle_call({:set_cursor_visibility, visible}, _from, renderer) do
set_cursor_in_terminal(visible, renderer.buffer)
{:reply, :ok, %{renderer | cursor_visible: visible}}
end
def handle_call({:set_title, title}, _from, renderer) do
# Set window title only if not in test mode
set_title_in_terminal()
{:reply, :ok, %{renderer | title: title}}
end
def handle_call(:get_title, _from, renderer) do
{:reply, renderer.title, renderer}
end
def handle_call(:init_terminal, _from, renderer) do
# Initialize termbox only if not in test mode
init_termbox()
{:reply, :ok, %{renderer | termbox_initialized: true}}
end
def handle_call(:shutdown_terminal, _from, renderer) do
# Shutdown termbox only if not in test mode
shutdown_termbox()
{:reply, :ok, %{renderer | termbox_initialized: false}}
end
def handle_call({:set_config_value, key, value}, _from, renderer) do
new_state =
case key do
:fps -> %{renderer | fps: value}
:theme -> %{renderer | theme: value}
:font_settings -> %{renderer | font_settings: value}
_ -> renderer
end
{:reply, :ok, new_state}
end
def handle_call(:reset_config, _from, renderer) do
new_state = %{
renderer
| fps: 60,
theme: %{},
font_settings: %{}
}
{:reply, :ok, new_state}
end
# Private functions
defp render_cell(col, row, cell) do
render_cell_in_terminal(col, row, cell)
end
defp render_to_terminal(state) do
:termbox2_nif.tb_init()
:termbox2_nif.tb_clear()
render_cells(state.buffer.cells)
render_cursor(state)
:termbox2_nif.tb_present()
end
defp render_cells(cells) do
Enum.each(cells, fn {row, cells} ->
Enum.each(cells, fn {col, cell} ->
render_cell(col, row, cell)
end)
end)
end
defp render_cursor(state) do
render_cursor_with_visibility(state.cursor_visible, state.buffer)
end
# Helper functions for if statement elimination
defp handle_render_request(false, _state, renderer) do
{:reply, {:error, :not_initialized}, renderer}
end
defp handle_render_request(true, state, renderer) do
unless Env.test?(), do: render_to_terminal(state)
{:reply, :ok, renderer}
end
defp cleanup_termbox do
unless Env.test?(), do: :termbox2_nif.tb_shutdown()
end
defp resize_termbox(width, height) do
unless Env.test?() do
:termbox2_nif.tb_set_cell(0, 0, 0, 0, 0)
:termbox2_nif.tb_set_cell(width - 1, height - 1, 0, 0, 0)
end
end
defp set_cursor_in_terminal(visible, buffer) do
unless Env.test?(), do: set_cursor_position(visible, buffer)
end
defp set_cursor_position(true, buffer) do
{x, y} = Buffer.get_cursor_position(buffer)
:termbox2_nif.tb_set_cursor(x, y)
end
defp set_cursor_position(false, _buffer) do
:termbox2_nif.tb_set_cursor(-1, -1)
end
defp set_title_in_terminal do
unless Env.test?(), do: :termbox2_nif.tb_set_cell(0, 0, 0, 0, 0)
end
defp init_termbox do
unless Env.test?(), do: :termbox2_nif.tb_init()
end
defp shutdown_termbox do
unless Env.test?(), do: :termbox2_nif.tb_shutdown()
end
defp render_cell_in_terminal(col, row, cell) do
unless Env.test?() do
:termbox2_nif.tb_set_cell(
col,
row,
cell.char,
cell.style.fg,
cell.style.bg
)
end
end
defp render_cursor_with_visibility(true, buffer) do
{x, y} = Buffer.get_cursor_position(buffer)
:termbox2_nif.tb_set_cursor(x, y)
end
defp render_cursor_with_visibility(false, _buffer) do
:termbox2_nif.tb_set_cursor(-1, -1)
end
end