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_manager.ex
defmodule Raxol.Terminal.Theme.Manager do
@moduledoc """
Manages terminal themes with advanced features:
- Theme loading from files and presets
- Theme customization and modification
- Dynamic theme switching
- Theme persistence and state management
## Unified Theme System
Themes are sourced from `Raxol.Core.Theming.ThemeRegistry`, which
provides a single source of truth for all Raxol themes. Use
`load_from_registry/2` to load any registered theme.
## Usage
manager = Raxol.Terminal.Theme.Manager.new()
# Load a theme from the unified registry
{:ok, manager} = Raxol.Terminal.Theme.Manager.load_from_registry(manager, :dracula)
# Get a style
{:ok, style, manager} = Raxol.Terminal.Theme.Manager.get_style(manager, :normal)
"""
# ThemeRegistry lives in main raxol; guarded at runtime
@compile {:no_warn_undefined, Raxol.Core.Theming.ThemeRegistry}
@type color :: %{
r: integer(),
g: integer(),
b: integer(),
a: float()
}
@type style :: %{
foreground: color(),
background: color(),
bold: boolean(),
italic: boolean(),
underline: boolean()
}
@type theme :: %{
name: String.t(),
description: String.t(),
author: String.t(),
version: String.t(),
colors: %{
background: color(),
foreground: color(),
cursor: color(),
selection: color(),
black: color(),
red: color(),
green: color(),
yellow: color(),
blue: color(),
magenta: color(),
cyan: color(),
white: color(),
bright_black: color(),
bright_red: color(),
bright_green: color(),
bright_yellow: color(),
bright_blue: color(),
bright_magenta: color(),
bright_cyan: color(),
bright_white: color()
},
styles: %{
normal: style(),
bold: style(),
italic: style(),
underline: style(),
cursor: style(),
selection: style()
}
}
@type t :: %__MODULE__{
current_theme: theme(),
themes: %{String.t() => theme()},
custom_styles: %{String.t() => style()},
metrics: %{
theme_switches: integer(),
style_applications: integer(),
customizations: integer(),
load_operations: integer()
}
}
defstruct [
:current_theme,
:themes,
:custom_styles,
:metrics
]
@doc """
Creates a new theme manager with the given options.
"""
@spec new(keyword()) :: t()
def new(_opts \\ []) do
default_theme = %{
name: "default",
description: "Default terminal theme",
author: "Raxol",
version: "1.0.1",
colors: %{
background: %{r: 0, g: 0, b: 0, a: 1.0},
foreground: %{r: 255, g: 255, b: 255, a: 1.0},
cursor: %{r: 255, g: 255, b: 255, a: 1.0},
selection: %{r: 51, g: 51, b: 51, a: 1.0},
black: %{r: 0, g: 0, b: 0, a: 1.0},
red: %{r: 255, g: 0, b: 0, a: 1.0},
green: %{r: 0, g: 255, b: 0, a: 1.0},
yellow: %{r: 255, g: 255, b: 0, a: 1.0},
blue: %{r: 0, g: 0, b: 255, a: 1.0},
magenta: %{r: 255, g: 0, b: 255, a: 1.0},
cyan: %{r: 0, g: 255, b: 255, a: 1.0},
white: %{r: 255, g: 255, b: 255, a: 1.0},
bright_black: %{r: 128, g: 128, b: 128, a: 1.0},
bright_red: %{r: 255, g: 128, b: 128, a: 1.0},
bright_green: %{r: 128, g: 255, b: 128, a: 1.0},
bright_yellow: %{r: 255, g: 255, b: 128, a: 1.0},
bright_blue: %{r: 128, g: 128, b: 255, a: 1.0},
bright_magenta: %{r: 255, g: 128, b: 255, a: 1.0},
bright_cyan: %{r: 128, g: 255, b: 255, a: 1.0},
bright_white: %{r: 255, g: 255, b: 255, a: 1.0}
},
styles: %{
normal: %{
foreground: %{r: 255, g: 255, b: 255, a: 1.0},
background: %{r: 0, g: 0, b: 0, a: 1.0},
bold: false,
italic: false,
underline: false
},
bold: %{
foreground: %{r: 255, g: 255, b: 255, a: 1.0},
background: %{r: 0, g: 0, b: 0, a: 1.0},
bold: true,
italic: false,
underline: false
},
italic: %{
foreground: %{r: 255, g: 255, b: 255, a: 1.0},
background: %{r: 0, g: 0, b: 0, a: 1.0},
bold: false,
italic: true,
underline: false
},
underline: %{
foreground: %{r: 255, g: 255, b: 255, a: 1.0},
background: %{r: 0, g: 0, b: 0, a: 1.0},
bold: false,
italic: false,
underline: true
},
cursor: %{
foreground: %{r: 0, g: 0, b: 0, a: 1.0},
background: %{r: 255, g: 255, b: 255, a: 1.0},
bold: false,
italic: false,
underline: false
},
selection: %{
foreground: %{r: 255, g: 255, b: 255, a: 1.0},
background: %{r: 51, g: 51, b: 51, a: 1.0},
bold: false,
italic: false,
underline: false
}
}
}
%__MODULE__{
current_theme: default_theme,
themes: %{"default" => default_theme},
custom_styles: %{},
metrics: %{
theme_switches: 0,
style_applications: 0,
customizations: 0,
load_operations: 0
}
}
end
@doc """
Loads a theme from a file or preset.
"""
@spec load_theme(t(), String.t()) :: {:ok, t()} | {:error, term()}
def load_theme(manager, theme_name) do
case Map.get(manager.themes, theme_name) do
nil ->
{:error, :theme_not_found}
theme ->
updated_manager = %{
manager
| current_theme: theme,
metrics: update_metrics(manager.metrics, :theme_switches)
}
{:ok, updated_manager}
end
end
@doc """
Loads a theme from the unified theme registry.
This is the preferred way to load themes as it uses the single
source of truth for all Raxol theme definitions.
## Examples
{:ok, manager} = Manager.load_from_registry(manager, :dracula)
{:ok, manager} = Manager.load_from_registry(manager, :synthwave84)
"""
@dialyzer {:nowarn_function, load_from_registry: 2}
@spec load_from_registry(t(), atom()) ::
{:ok, t()} | {:error, :theme_not_found}
def load_from_registry(manager, theme_name) when is_atom(theme_name) do
case Raxol.Core.Theming.ThemeRegistry.to_terminal_format(theme_name) do
nil ->
{:error, :theme_not_found}
theme when is_map(theme) ->
# Add to available themes and set as current
updated_themes = Map.put(manager.themes, theme.name, theme)
updated_manager = %{
manager
| current_theme: theme,
themes: updated_themes,
metrics: update_metrics(manager.metrics, :theme_switches)
}
{:ok, updated_manager}
end
end
@doc """
Lists all themes available in the unified registry.
"""
@spec list_registry_themes() :: [atom()]
def list_registry_themes do
Raxol.Core.Theming.ThemeRegistry.list()
end
@doc """
Adds a custom style to the current theme.
"""
@spec add_custom_style(t(), String.t(), style()) ::
{:ok, t()} | {:error, term()}
def add_custom_style(manager, name, style) do
case validate_style(style) do
:ok ->
new_styles = Map.put(manager.custom_styles, name, style)
updated_manager = %{
manager
| custom_styles: new_styles,
metrics: update_metrics(manager.metrics, :customizations)
}
{:ok, updated_manager}
{:error, reason} ->
{:error, reason}
end
end
@doc """
Gets a style from the current theme or custom styles.
"""
@spec get_style(t(), String.t() | atom()) ::
{:ok, style(), t()} | {:error, term()}
def get_style(manager, style_name) do
# Try atom and string keys for theme styles
style =
Map.get(manager.current_theme.styles, style_name) ||
Map.get(manager.current_theme.styles, String.to_atom(style_name))
case style do
nil ->
case Map.get(manager.custom_styles, style_name) do
nil ->
{:error, :style_not_found}
style ->
updated_manager = %{
manager
| metrics: update_metrics(manager.metrics, :style_applications)
}
{:ok, style, updated_manager}
end
style ->
updated_manager = %{
manager
| metrics: update_metrics(manager.metrics, :style_applications)
}
{:ok, style, updated_manager}
end
end
@doc """
Gets the current theme metrics.
"""
@spec get_metrics(t()) :: map()
def get_metrics(manager) do
manager.metrics
end
@doc """
Saves the current theme state for persistence.
"""
@spec save_theme_state(t()) :: {:ok, map()}
def save_theme_state(manager) do
state = %{
current_theme: manager.current_theme.name,
custom_styles: manager.custom_styles
}
{:ok, state}
end
@doc """
Restores a theme state from saved data.
"""
@spec restore_theme_state(t(), map()) :: {:ok, t()} | {:error, term()}
def restore_theme_state(manager, state) do
with {:ok, manager} <- load_theme(manager, state.current_theme) do
updated_manager = %{
manager
| custom_styles: state.custom_styles,
metrics: update_metrics(manager.metrics, :load_operations)
}
{:ok, updated_manager}
end
end
# Private helper functions
defp validate_style(style) do
required_fields = [:foreground, :background, :bold, :italic, :underline]
case Enum.all?(required_fields, &Map.has_key?(style, &1)) do
true -> :ok
false -> {:error, :invalid_style}
end
end
defp update_metrics(metrics, :theme_switches) do
%{metrics | theme_switches: metrics.theme_switches + 1}
end
defp update_metrics(metrics, :style_applications) do
%{metrics | style_applications: metrics.style_applications + 1}
end
defp update_metrics(metrics, :customizations) do
%{metrics | customizations: metrics.customizations + 1}
end
defp update_metrics(metrics, :load_operations) do
%{metrics | load_operations: metrics.load_operations + 1}
end
end