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