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
Current section
Files
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