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 theme theme_server.ex
Raw

lib/raxol/terminal/theme/theme_server.ex

defmodule Raxol.Terminal.Theme.ThemeServer do
@moduledoc """
Unified theme system for the Raxol terminal emulator.
Handles theme management, preview, switching, and customization.
"""
use Raxol.Core.Behaviours.BaseManager
alias Raxol.Core.Runtime.Log
# Types
@type theme_id :: String.t()
@type theme_state :: %{
id: theme_id(),
name: String.t(),
version: String.t(),
description: String.t(),
author: String.t(),
colors: map(),
font: map(),
cursor: map(),
padding: map(),
status: :active | :inactive | :error,
error: String.t() | nil
}
# Client API
# BaseManager provides start_link/1 automatically with name: __MODULE__ as default
@doc """
Loads a theme from a file or directory.
"""
def load_theme(path, opts \\ []) do
GenServer.call(__MODULE__, {:load_theme, path, opts})
end
@doc """
Unloads a theme by ID.
"""
def unload_theme(theme_id) do
GenServer.call(__MODULE__, {:unload_theme, theme_id})
end
@doc """
Gets the state of a theme.
"""
def get_theme_state(theme_id) do
GenServer.call(__MODULE__, {:get_theme_state, theme_id})
end
@doc """
Gets all loaded themes.
"""
def get_themes(opts \\ []) do
GenServer.call(__MODULE__, {:get_themes, opts})
end
@doc """
Updates a theme's configuration.
"""
def update_theme_config(theme_id, config) do
GenServer.call(__MODULE__, {:update_theme_config, theme_id, config})
end
@doc """
Applies a theme to the terminal.
"""
def apply_theme(theme_id) do
GenServer.call(__MODULE__, {:apply_theme, theme_id})
end
@doc """
Previews a theme without applying it.
"""
def preview_theme(theme_id) do
GenServer.call(__MODULE__, {:preview_theme, theme_id})
end
@doc """
Exports a theme to a file.
"""
def export_theme(theme_id, path) do
GenServer.call(__MODULE__, {:export_theme, theme_id, path})
end
@doc """
Imports a theme from a file.
"""
def import_theme(path) do
GenServer.call(__MODULE__, {:import_theme, path})
end
# Server Callbacks
@impl true
def init_manager(opts) do
opts_map =
case opts do
opts when is_list(opts) -> Map.new(opts)
opts when is_map(opts) -> opts
end
state = %{
themes: %{},
theme_paths: Map.get(opts_map, :theme_paths, []),
auto_load: Map.get(opts_map, :auto_load, true),
current_theme: nil,
preview_theme: nil,
theme_config: Map.get(opts_map, :theme_config, %{})
}
case state.auto_load do
true -> load_themes_from_paths(state.theme_paths)
false -> :ok
end
{:ok, state}
end
@impl true
def handle_manager_call({:load_theme, path, opts}, _from, state) do
case do_load_theme(path, opts, state) do
{:ok, theme_id, theme_state} ->
new_state = put_in(state.themes[theme_id], theme_state)
{:reply, {:ok, theme_id}, new_state}
{:error, reason} ->
{:reply, {:error, reason}, state}
end
end
@impl true
def handle_manager_call({:unload_theme, theme_id}, _from, state) do
case do_unload_theme(theme_id, state) do
{:ok, new_state} ->
{:reply, :ok, new_state}
{:error, reason} ->
{:reply, {:error, reason}, state}
end
end
@impl true
def handle_manager_call({:get_theme_state, theme_id}, _from, state) do
case Map.get(state.themes, theme_id) do
nil -> {:reply, {:error, :theme_not_found}, state}
theme_state -> {:reply, {:ok, theme_state}, state}
end
end
@impl true
def handle_manager_call({:get_themes, opts}, _from, state) do
themes = filter_themes(state.themes, opts)
{:reply, {:ok, themes}, state}
end
@impl true
def handle_manager_call(
{:update_theme_config, theme_id, config},
_from,
state
) do
case do_update_theme_config(theme_id, config, state) do
{:ok, new_state} ->
{:reply, :ok, new_state}
{:error, reason} ->
{:reply, {:error, reason}, state}
end
end
@impl true
def handle_manager_call({:apply_theme, theme_id}, _from, state) do
case do_apply_theme(theme_id, state) do
{:ok, new_state} ->
{:reply, :ok, new_state}
{:error, reason} ->
{:reply, {:error, reason}, state}
end
end
@impl true
def handle_manager_call({:preview_theme, theme_id}, _from, state) do
case do_preview_theme(theme_id, state) do
{:ok, new_state} ->
{:reply, :ok, new_state}
{:error, reason} ->
{:reply, {:error, reason}, state}
end
end
@impl true
def handle_manager_call({:export_theme, theme_id, path}, _from, state) do
case do_export_theme(theme_id, path, state) do
{:ok, new_state} ->
{:reply, :ok, new_state}
{:error, reason} ->
{:reply, {:error, reason}, state}
end
end
@impl true
def handle_manager_call({:import_theme, path}, _from, state) do
case do_import_theme(path, state) do
{:ok, theme_id, new_state} ->
{:reply, {:ok, theme_id}, new_state}
{:error, reason} ->
{:reply, {:error, reason}, state}
end
end
# Private Functions
defp do_load_theme(path, opts, _state) do
with {:ok, theme_id} <- generate_theme_id(path),
{:ok, theme_state} <- load_theme_state(path, opts),
:ok <- validate_theme(theme_state) do
{:ok, theme_id, theme_state}
else
{:error, reason} -> {:error, reason}
end
end
defp do_unload_theme(theme_id, state) do
case Map.get(state.themes, theme_id) do
nil ->
{:error, :theme_not_found}
theme_state ->
case cleanup_theme(theme_state) do
:ok ->
new_state = update_in(state.themes, &Map.delete(&1, theme_id))
{:ok, new_state}
end
end
end
defp do_update_theme_config(theme_id, config, state) do
case Map.get(state.themes, theme_id) do
nil ->
{:error, :theme_not_found}
theme_state ->
case validate_theme_config(config) do
:ok ->
new_theme_state = put_in(theme_state.config, config)
new_state = put_in(state.themes[theme_id], new_theme_state)
{:ok, new_state}
{:error, reason} ->
{:error, reason}
end
end
end
defp do_apply_theme(theme_id, state) do
case Map.get(state.themes, theme_id) do
nil ->
{:error, :theme_not_found}
theme_state ->
case theme_state.status do
:active ->
new_state = %{state | current_theme: theme_id}
{:ok, new_state}
:inactive ->
{:error, :theme_inactive}
:error ->
{:error, :theme_error}
end
end
end
defp do_preview_theme(theme_id, state) do
case Map.get(state.themes, theme_id) do
nil ->
{:error, :theme_not_found}
theme_state ->
case theme_state.status do
:active ->
new_state = %{state | preview_theme: theme_id}
{:ok, new_state}
:inactive ->
{:error, :theme_inactive}
:error ->
{:error, :theme_error}
end
end
end
defp do_export_theme(theme_id, path, state) do
case Map.get(state.themes, theme_id) do
nil ->
{:error, :theme_not_found}
theme_state ->
case export_theme_to_file(theme_state, path) do
:ok -> {:ok, state}
{:error, reason} -> {:error, reason}
end
end
end
defp do_import_theme(path, state) do
case import_theme_from_file(path) do
{:ok, theme_state} ->
theme_id = generate_theme_id(path)
new_state = put_in(state.themes[theme_id], theme_state)
{:ok, theme_id, new_state}
{:error, reason} ->
{:error, reason}
end
end
defp generate_theme_id(path) do
{:ok, :crypto.hash(:sha256, path) |> Base.encode16()}
end
defp load_theme_state(path, _opts) do
parse_theme_file(path)
end
defp validate_theme(theme_state) do
required_fields = [
:name,
:version,
:description,
:author,
:colors,
:font,
:cursor,
:padding
]
case Enum.all?(required_fields, fn field ->
Map.has_key?(theme_state, field) and
Map.get(theme_state, field) != nil
end) do
true -> :ok
false -> {:error, :invalid_theme_format}
end
end
defp validate_theme_config(config) do
case is_map(config) do
true -> :ok
false -> {:error, :invalid_config_format}
end
end
defp cleanup_theme(_theme_state) do
# In a real implementation, this would clean up any resources used by the theme
:ok
end
defp filter_themes(themes, opts) do
themes
|> Enum.filter(fn {_id, theme} ->
Enum.all?(opts, fn
{:status, status} -> theme.status == status
_ -> true
end)
end)
|> Map.new()
end
defp load_themes_from_paths(paths) do
Enum.each(paths, &load_themes_from_path/1)
end
defp load_themes_from_path(path) do
case File.ls(path) do
{:ok, files} ->
Enum.each(files, &load_theme_from_path(path, &1))
{:error, reason} ->
Log.error("Failed to list theme directory #{path}: #{reason}")
end
end
defp load_theme_from_path(base_path, file) do
full_path = Path.join(base_path, file)
case File.dir?(full_path) do
true -> load_theme_from_directory(full_path)
false -> load_theme_from_file(full_path)
end
end
defp load_theme_from_directory(_path) do
# Implementation for loading theme from directory
:ok
end
defp load_theme_from_file(_path) do
# Implementation for loading theme from file
:ok
end
defp export_theme_to_file(theme_state, path) do
case Jason.encode(theme_state) do
{:ok, json} ->
case File.write(path, json) do
:ok -> :ok
{:error, reason} -> {:error, {:file_write_error, reason}}
end
{:error, reason} ->
{:error, {:json_encode_error, reason}}
end
end
defp import_theme_from_file(path) do
parse_theme_file(path)
end
defp parse_theme_file(path) do
case File.read(path) do
{:ok, content} ->
case Jason.decode(content) do
{:ok, theme_data} ->
{:ok, build_theme_state(theme_data)}
{:error, reason} ->
{:error, {:invalid_theme_format, reason}}
end
{:error, reason} ->
{:error, {:file_read_error, reason}}
end
end
defp build_theme_state(theme_data) do
%{
# Will be set by generate_theme_id
id: nil,
name: theme_data["name"],
version: theme_data["version"],
description: theme_data["description"],
author: theme_data["author"],
colors: theme_data["colors"],
font: theme_data["font"],
cursor: theme_data["cursor"],
padding: theme_data["padding"],
status: :active,
error: nil,
config: %{}
}
end
end