Skip to main content

lipgloss/
theme.rs

1//! Theme system with semantic color slots.
2//!
3//! The [`Theme`] struct provides semantic color slots that components can reference
4//! for consistent styling across an application. Themes support light/dark variants
5//! and can be serialized for user configuration.
6//!
7//! ## Preset Preview
8//!
9//! <table>
10//!   <tr><th>Preset</th><th>Background</th><th>Primary</th><th>Text</th></tr>
11//!   <tr><td>Dark</td><td><span style="display:inline-block;width:0.9em;height:0.9em;background:#0f0f0f;border:1px solid #999"></span> `#0f0f0f`</td><td><span style="display:inline-block;width:0.9em;height:0.9em;background:#7c3aed;border:1px solid #999"></span> `#7c3aed`</td><td><span style="display:inline-block;width:0.9em;height:0.9em;background:#fafafa;border:1px solid #999"></span> `#fafafa`</td></tr>
12//!   <tr><td>Light</td><td><span style="display:inline-block;width:0.9em;height:0.9em;background:#ffffff;border:1px solid #999"></span> `#ffffff`</td><td><span style="display:inline-block;width:0.9em;height:0.9em;background:#7c3aed;border:1px solid #999"></span> `#7c3aed`</td><td><span style="display:inline-block;width:0.9em;height:0.9em;background:#18181b;border:1px solid #999"></span> `#18181b`</td></tr>
13//!   <tr><td>Dracula</td><td><span style="display:inline-block;width:0.9em;height:0.9em;background:#282a36;border:1px solid #999"></span> `#282a36`</td><td><span style="display:inline-block;width:0.9em;height:0.9em;background:#bd93f9;border:1px solid #999"></span> `#bd93f9`</td><td><span style="display:inline-block;width:0.9em;height:0.9em;background:#f8f8f2;border:1px solid #999"></span> `#f8f8f2`</td></tr>
14//!   <tr><td>Nord</td><td><span style="display:inline-block;width:0.9em;height:0.9em;background:#2e3440;border:1px solid #999"></span> `#2e3440`</td><td><span style="display:inline-block;width:0.9em;height:0.9em;background:#88c0d0;border:1px solid #999"></span> `#88c0d0`</td><td><span style="display:inline-block;width:0.9em;height:0.9em;background:#eceff4;border:1px solid #999"></span> `#eceff4`</td></tr>
15//!   <tr><td>Catppuccin Latte</td><td><span style="display:inline-block;width:0.9em;height:0.9em;background:#eff1f5;border:1px solid #999"></span> `#eff1f5`</td><td><span style="display:inline-block;width:0.9em;height:0.9em;background:#8839ef;border:1px solid #999"></span> `#8839ef`</td><td><span style="display:inline-block;width:0.9em;height:0.9em;background:#4c4f69;border:1px solid #999"></span> `#4c4f69`</td></tr>
16//!   <tr><td>Catppuccin Frappe</td><td><span style="display:inline-block;width:0.9em;height:0.9em;background:#303446;border:1px solid #999"></span> `#303446`</td><td><span style="display:inline-block;width:0.9em;height:0.9em;background:#ca9ee6;border:1px solid #999"></span> `#ca9ee6`</td><td><span style="display:inline-block;width:0.9em;height:0.9em;background:#c6d0f5;border:1px solid #999"></span> `#c6d0f5`</td></tr>
17//!   <tr><td>Catppuccin Macchiato</td><td><span style="display:inline-block;width:0.9em;height:0.9em;background:#24273a;border:1px solid #999"></span> `#24273a`</td><td><span style="display:inline-block;width:0.9em;height:0.9em;background:#c6a0f6;border:1px solid #999"></span> `#c6a0f6`</td><td><span style="display:inline-block;width:0.9em;height:0.9em;background:#cad3f5;border:1px solid #999"></span> `#cad3f5`</td></tr>
18//!   <tr><td>Catppuccin Mocha</td><td><span style="display:inline-block;width:0.9em;height:0.9em;background:#1e1e2e;border:1px solid #999"></span> `#1e1e2e`</td><td><span style="display:inline-block;width:0.9em;height:0.9em;background:#cba6f7;border:1px solid #999"></span> `#cba6f7`</td><td><span style="display:inline-block;width:0.9em;height:0.9em;background:#cdd6f4;border:1px solid #999"></span> `#cdd6f4`</td></tr>
19//! </table>
20//!
21//! # Example
22//!
23//! ```rust
24//! use lipgloss::theme::{Theme, ThemeColors};
25//!
26//! // Use the default dark theme
27//! let theme = Theme::dark();
28//!
29//! // Create a style using theme colors
30//! let style = theme.style()
31//!     .foreground_color(theme.colors().primary.clone())
32//!     .background_color(theme.colors().background.clone());
33//! ```
34
35use crate::border::Border;
36use crate::color::{AdaptiveColor, Color, ansi256_to_rgb};
37use crate::position::{Position, Sides};
38use crate::renderer::Renderer;
39use crate::style::Style;
40use serde::{Deserialize, Serialize};
41use std::collections::HashMap;
42use std::fmt;
43#[cfg(feature = "native")]
44use std::fs;
45use std::panic::{AssertUnwindSafe, catch_unwind};
46#[cfg(feature = "native")]
47use std::path::Path;
48use std::sync::atomic::{AtomicU64, Ordering};
49use std::sync::{Arc, LazyLock, RwLock, RwLockReadGuard, RwLockWriteGuard};
50use thiserror::Error;
51use tracing::{debug, info, trace, warn};
52
53/// A complete theme with semantic color slots.
54///
55/// Themes provide a consistent color palette that components can reference
56/// by semantic meaning (e.g., "primary", "error") rather than raw color values.
57/// This enables easy theme switching and ensures visual consistency.
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct Theme {
60    /// Human-readable name for this theme.
61    #[serde(default)]
62    name: String,
63
64    /// Whether this is a dark theme (affects adaptive color selection).
65    #[serde(default)]
66    is_dark: bool,
67
68    /// The color palette.
69    #[serde(default)]
70    colors: ThemeColors,
71
72    /// Optional theme description.
73    #[serde(default, skip_serializing_if = "Option::is_none")]
74    description: Option<String>,
75
76    /// Optional theme author.
77    #[serde(default, skip_serializing_if = "Option::is_none")]
78    author: Option<String>,
79
80    /// Optional theme metadata.
81    #[serde(default, skip_serializing_if = "ThemeMeta::is_empty")]
82    meta: ThemeMeta,
83}
84
85/// Semantic color slots for a theme.
86///
87/// Each slot represents a semantic purpose rather than a specific color.
88/// This allows the same code to work with different themes while maintaining
89/// appropriate visual meaning.
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct ThemeColors {
92    // ========================
93    // Primary Palette
94    // ========================
95    /// Primary brand/accent color. Used for primary actions, links, and emphasis.
96    pub primary: Color,
97
98    /// Secondary color. Used for secondary actions and less prominent elements.
99    pub secondary: Color,
100
101    /// Accent color. Used for highlights, indicators, and visual interest.
102    pub accent: Color,
103
104    // ========================
105    // Background Colors
106    // ========================
107    /// Main background color.
108    pub background: Color,
109
110    /// Elevated surface color (cards, dialogs, popups).
111    pub surface: Color,
112
113    /// Alternative surface for visual layering.
114    pub surface_alt: Color,
115
116    // ========================
117    // Text Colors
118    // ========================
119    /// Primary text color (high contrast, main content).
120    pub text: Color,
121
122    /// Muted text color (secondary content, descriptions).
123    pub text_muted: Color,
124
125    /// Disabled text color (inactive elements).
126    pub text_disabled: Color,
127
128    // ========================
129    // Semantic Colors
130    // ========================
131    /// Success/positive color (confirmations, success states).
132    pub success: Color,
133
134    /// Warning color (cautions, alerts).
135    pub warning: Color,
136
137    /// Error/danger color (errors, destructive actions).
138    pub error: Color,
139
140    /// Info color (informational messages, neutral highlights).
141    pub info: Color,
142
143    // ========================
144    // UI Element Colors
145    // ========================
146    /// Border color for UI elements.
147    pub border: Color,
148
149    /// Subtle border color (dividers, separators).
150    pub border_muted: Color,
151
152    /// Separator/divider color.
153    pub separator: Color,
154
155    // ========================
156    // Interactive States
157    // ========================
158    /// Focus indicator color.
159    pub focus: Color,
160
161    /// Selection/highlight background color.
162    pub selection: Color,
163
164    /// Hover state color.
165    pub hover: Color,
166
167    // ========================
168    // Code/Syntax Colors
169    // ========================
170    /// Code/syntax: Keywords (if, else, fn, etc.)
171    pub code_keyword: Color,
172
173    /// Code/syntax: Strings
174    pub code_string: Color,
175
176    /// Code/syntax: Numbers
177    pub code_number: Color,
178
179    /// Code/syntax: Comments
180    pub code_comment: Color,
181
182    /// Code/syntax: Function names
183    pub code_function: Color,
184
185    /// Code/syntax: Types/classes
186    pub code_type: Color,
187
188    /// Code/syntax: Variables
189    pub code_variable: Color,
190
191    /// Code/syntax: Operators
192    pub code_operator: Color,
193
194    /// Custom color slots.
195    #[serde(default, flatten)]
196    pub custom: HashMap<String, Color>,
197}
198
199/// Named color slots for referencing theme colors.
200#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
201pub enum ColorSlot {
202    /// Primary brand/accent color.
203    Primary,
204    /// Secondary accent color.
205    Secondary,
206    /// Highlight/accent color.
207    Accent,
208    /// Background color.
209    Background,
210    /// Foreground/text color (alias of `Text`).
211    Foreground,
212    /// Primary text color.
213    Text,
214    /// Muted/secondary text color.
215    TextMuted,
216    /// Disabled text color.
217    TextDisabled,
218    /// Elevated surface color.
219    Surface,
220    /// Alternative surface color.
221    SurfaceAlt,
222    /// Success/positive color.
223    Success,
224    /// Warning color.
225    Warning,
226    /// Error/danger color.
227    Error,
228    /// Informational color.
229    Info,
230    /// Border color.
231    Border,
232    /// Subtle border color.
233    BorderMuted,
234    /// Divider/separator color.
235    Separator,
236    /// Focus indicator color.
237    Focus,
238    /// Selection/background highlight color.
239    Selection,
240    /// Hover state color.
241    Hover,
242    /// Code/syntax keyword color.
243    CodeKeyword,
244    /// Code/syntax string color.
245    CodeString,
246    /// Code/syntax number color.
247    CodeNumber,
248    /// Code/syntax comment color.
249    CodeComment,
250    /// Code/syntax function color.
251    CodeFunction,
252    /// Code/syntax type color.
253    CodeType,
254    /// Code/syntax variable color.
255    CodeVariable,
256    /// Code/syntax operator color.
257    CodeOperator,
258}
259
260/// Semantic roles for quick style creation.
261#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
262pub enum ThemeRole {
263    /// Primary/brand role.
264    Primary,
265    /// Success/positive role.
266    Success,
267    /// Warning role.
268    Warning,
269    /// Error/danger role.
270    Error,
271    /// Muted/secondary role.
272    Muted,
273    /// Inverted role (swap foreground/background).
274    Inverted,
275}
276
277/// Built-in theme presets.
278#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
279pub enum ThemePreset {
280    Dark,
281    Light,
282    Dracula,
283    Nord,
284    Catppuccin(CatppuccinFlavor),
285}
286
287/// Catppuccin palette flavors.
288#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
289pub enum CatppuccinFlavor {
290    /// Light flavor.
291    Latte,
292    /// Medium-light flavor.
293    Frappe,
294    /// Medium-dark flavor.
295    Macchiato,
296    /// Dark flavor.
297    Mocha,
298}
299
300impl fmt::Display for CatppuccinFlavor {
301    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
302        let name = match self {
303            Self::Latte => "Latte",
304            Self::Frappe => "Frappe",
305            Self::Macchiato => "Macchiato",
306            Self::Mocha => "Mocha",
307        };
308        f.write_str(name)
309    }
310}
311
312impl fmt::Display for ThemePreset {
313    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
314        match self {
315            Self::Dark => f.write_str("Dark"),
316            Self::Light => f.write_str("Light"),
317            Self::Dracula => f.write_str("Dracula"),
318            Self::Nord => f.write_str("Nord"),
319            Self::Catppuccin(flavor) => write!(f, "Catppuccin {flavor}"),
320        }
321    }
322}
323
324/// Theme variant for light/dark themes.
325#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
326#[serde(rename_all = "lowercase")]
327pub enum ThemeVariant {
328    Light,
329    Dark,
330}
331
332/// Optional metadata for themes.
333#[derive(Debug, Clone, Default, Serialize, Deserialize)]
334pub struct ThemeMeta {
335    #[serde(default, skip_serializing_if = "Option::is_none")]
336    pub version: Option<String>,
337    #[serde(default, skip_serializing_if = "Option::is_none")]
338    pub variant: Option<ThemeVariant>,
339    #[serde(default, skip_serializing_if = "Option::is_none")]
340    pub source: Option<String>,
341}
342
343impl ThemeMeta {
344    fn is_empty(&self) -> bool {
345        self.version.is_none() && self.variant.is_none() && self.source.is_none()
346    }
347}
348
349/// Identifier for a registered theme change listener.
350#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
351pub struct ListenerId(u64);
352
353/// Listener callback for theme changes.
354pub trait ThemeChangeListener: Send + Sync {
355    fn on_theme_change(&self, theme: &Theme);
356}
357
358impl<F> ThemeChangeListener for F
359where
360    F: Fn(&Theme) + Send + Sync,
361{
362    fn on_theme_change(&self, theme: &Theme) {
363        self(theme);
364    }
365}
366
367/// Thread-safe context for runtime theme switching.
368#[derive(Clone)]
369pub struct ThemeContext {
370    current: Arc<RwLock<Theme>>,
371    listeners: Arc<RwLock<HashMap<ListenerId, Arc<dyn ThemeChangeListener>>>>,
372    next_listener_id: Arc<AtomicU64>,
373}
374
375fn read_lock_or_recover<'a, T>(lock: &'a RwLock<T>, lock_name: &str) -> RwLockReadGuard<'a, T> {
376    match lock.read() {
377        Ok(guard) => guard,
378        Err(poisoned) => {
379            warn!(lock = lock_name, "Recovering from poisoned read lock");
380            poisoned.into_inner()
381        }
382    }
383}
384
385fn write_lock_or_recover<'a, T>(lock: &'a RwLock<T>, lock_name: &str) -> RwLockWriteGuard<'a, T> {
386    match lock.write() {
387        Ok(guard) => guard,
388        Err(poisoned) => {
389            warn!(lock = lock_name, "Recovering from poisoned write lock");
390            poisoned.into_inner()
391        }
392    }
393}
394
395impl fmt::Debug for ThemeContext {
396    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
397        let listener_count = read_lock_or_recover(&self.listeners, "theme.listeners").len();
398        f.debug_struct("ThemeContext")
399            .field("current", &"<RwLock<Theme>>")
400            .field("listeners", &format!("{listener_count} listeners"))
401            .field("next_listener_id", &self.next_listener_id)
402            .finish()
403    }
404}
405
406impl ThemeContext {
407    /// Create a new context with the provided theme.
408    pub fn new(initial: Theme) -> Self {
409        Self {
410            current: Arc::new(RwLock::new(initial)),
411            listeners: Arc::new(RwLock::new(HashMap::new())),
412            next_listener_id: Arc::new(AtomicU64::new(1)),
413        }
414    }
415
416    /// Create a new context from a preset.
417    pub fn from_preset(preset: ThemePreset) -> Self {
418        Self::new(preset.to_theme())
419    }
420
421    /// Returns a read guard for the current theme.
422    pub fn current(&self) -> std::sync::RwLockReadGuard<'_, Theme> {
423        let guard = read_lock_or_recover(&self.current, "theme.current");
424        trace!(theme.name = %guard.name(), "Theme read");
425        guard
426    }
427
428    /// Switch to a new theme and notify listeners.
429    pub fn set_theme(&self, theme: Theme) {
430        let from = {
431            let current = read_lock_or_recover(&self.current, "theme.current");
432            current.name().to_string()
433        };
434        let to = theme.name().to_string();
435        let snapshot = theme.clone();
436        {
437            let mut current = write_lock_or_recover(&self.current, "theme.current");
438            *current = theme;
439        }
440
441        info!(theme.from = %from, theme.to = %to, "Theme switched");
442        self.notify_listeners(&snapshot);
443    }
444
445    /// Switch to a preset theme and notify listeners.
446    pub fn set_preset(&self, preset: ThemePreset) {
447        self.set_theme(preset.to_theme());
448    }
449
450    /// Register a listener for theme changes.
451    pub fn on_change<F>(&self, callback: F) -> ListenerId
452    where
453        F: Fn(&Theme) + Send + Sync + 'static,
454    {
455        let id = ListenerId(self.next_listener_id.fetch_add(1, Ordering::Relaxed));
456        write_lock_or_recover(&self.listeners, "theme.listeners").insert(id, Arc::new(callback));
457        debug!(theme.listener_id = id.0, "Theme listener registered");
458        id
459    }
460
461    /// Remove a listener by id.
462    pub fn remove_listener(&self, id: ListenerId) {
463        let mut listeners = write_lock_or_recover(&self.listeners, "theme.listeners");
464        if listeners.remove(&id).is_some() {
465            debug!(theme.listener_id = id.0, "Theme listener removed");
466        }
467    }
468
469    fn notify_listeners(&self, theme: &Theme) {
470        let listeners: Vec<(ListenerId, Arc<dyn ThemeChangeListener>)> = {
471            let listeners = read_lock_or_recover(&self.listeners, "theme.listeners");
472            listeners
473                .iter()
474                .map(|(id, listener)| (*id, Arc::clone(listener)))
475                .collect()
476        };
477
478        for (id, listener) in listeners {
479            let result = catch_unwind(AssertUnwindSafe(|| listener.on_theme_change(theme)));
480            if result.is_err() {
481                warn!(
482                    theme.listener_id = id.0,
483                    theme.name = %theme.name(),
484                    "Theme listener panicked"
485                );
486            }
487        }
488    }
489}
490
491static GLOBAL_THEME_CONTEXT: LazyLock<ThemeContext> =
492    LazyLock::new(|| ThemeContext::from_preset(ThemePreset::Dark));
493
494/// Returns the global theme context.
495pub fn global_theme() -> &'static ThemeContext {
496    &GLOBAL_THEME_CONTEXT
497}
498
499/// Replace the global theme.
500pub fn set_global_theme(theme: Theme) {
501    GLOBAL_THEME_CONTEXT.set_theme(theme);
502}
503
504/// Replace the global theme using a preset.
505pub fn set_global_preset(preset: ThemePreset) {
506    GLOBAL_THEME_CONTEXT.set_preset(preset);
507}
508
509// -----------------------------------------------------------------------------
510// Themed Styles (auto-resolve colors from ThemeContext)
511// -----------------------------------------------------------------------------
512
513/// A style that automatically resolves colors from the current theme.
514#[derive(Clone, Debug)]
515pub struct ThemedStyle {
516    context: Arc<ThemeContext>,
517    foreground: Option<ThemedColor>,
518    background: Option<ThemedColor>,
519    border_foreground: Option<ThemedColor>,
520    border_background: Option<ThemedColor>,
521    base_style: Style,
522}
523
524/// Color that can be fixed, sourced from a theme slot, or computed.
525#[derive(Clone, Debug)]
526pub enum ThemedColor {
527    /// Fixed color value.
528    Fixed(Color),
529    /// Color resolved from theme at render time.
530    Slot(ColorSlot),
531    /// Computed color based on a theme slot.
532    Computed(ColorSlot, ColorTransform),
533}
534
535/// Transformations applied to theme colors at resolve time.
536#[derive(Clone, Copy, Debug)]
537pub enum ColorTransform {
538    /// Lighten by 0.0-1.0.
539    Lighten(f32),
540    /// Darken by 0.0-1.0.
541    Darken(f32),
542    /// Increase saturation by 0.0-1.0.
543    Saturate(f32),
544    /// Decrease saturation by 0.0-1.0.
545    Desaturate(f32),
546    /// Apply alpha (approximated for terminal colors).
547    Alpha(f32),
548}
549
550impl ThemedStyle {
551    /// Create a new themed style with a context.
552    pub fn new(context: Arc<ThemeContext>) -> Self {
553        Self {
554            context,
555            foreground: None,
556            background: None,
557            border_foreground: None,
558            border_background: None,
559            base_style: Style::new(),
560        }
561    }
562
563    /// Create from the global theme context.
564    pub fn global() -> Self {
565        Self::new(Arc::new(global_theme().clone()))
566    }
567
568    /// Resolve the themed style to a concrete Style using the current theme.
569    pub fn resolve(&self) -> Style {
570        let Ok(theme) = catch_unwind(AssertUnwindSafe(|| self.context.current())) else {
571            warn!("themed_style.resolve called without a valid theme context");
572            return self.base_style.clone();
573        };
574
575        let mut style = self.base_style.clone();
576
577        if let Some(ref fg) = self.foreground {
578            let color = Self::resolve_color(fg, &theme);
579            style = style.foreground_color(color);
580        }
581        if let Some(ref bg) = self.background {
582            let color = Self::resolve_color(bg, &theme);
583            style = style.background_color(color);
584        }
585        if let Some(ref bfg) = self.border_foreground {
586            let color = Self::resolve_color(bfg, &theme);
587            style = style.border_foreground(color.0);
588        }
589        if let Some(ref bbg) = self.border_background {
590            let color = Self::resolve_color(bbg, &theme);
591            style = style.border_background(color.0);
592        }
593
594        drop(theme);
595        style
596    }
597
598    /// Render text with the themed style (resolves at call time).
599    pub fn render(&self, text: &str) -> String {
600        self.resolve().render(text)
601    }
602
603    fn resolve_color(themed: &ThemedColor, theme: &Theme) -> Color {
604        match themed {
605            ThemedColor::Fixed(color) => {
606                debug!(themed_style.resolve = true, color_kind = "fixed", color = %color.0);
607                color.clone()
608            }
609            ThemedColor::Slot(slot) => {
610                let color = theme.get(*slot);
611                debug!(
612                    themed_style.resolve = true,
613                    color_kind = "slot",
614                    color_slot = ?slot,
615                    color = %color.0
616                );
617                color
618            }
619            ThemedColor::Computed(slot, transform) => {
620                let base = theme.get(*slot);
621                let color = transform.apply(base);
622                debug!(
623                    themed_style.resolve = true,
624                    color_kind = "computed",
625                    color_slot = ?slot,
626                    transform = ?transform,
627                    color = %color.0
628                );
629                color
630            }
631        }
632    }
633
634    // ---------------------------------------------------------------------
635    // Theme-aware color setters
636    // ---------------------------------------------------------------------
637
638    /// Set foreground to a theme color slot.
639    pub fn foreground(mut self, slot: ColorSlot) -> Self {
640        self.foreground = Some(ThemedColor::Slot(slot));
641        self
642    }
643
644    /// Set foreground to a fixed color (ignores theme).
645    pub fn foreground_fixed(mut self, color: impl Into<Color>) -> Self {
646        self.foreground = Some(ThemedColor::Fixed(color.into()));
647        self
648    }
649
650    /// Set foreground to a computed theme color.
651    pub fn foreground_computed(mut self, slot: ColorSlot, transform: ColorTransform) -> Self {
652        self.foreground = Some(ThemedColor::Computed(slot, transform));
653        self
654    }
655
656    /// Clear any themed foreground.
657    pub fn no_foreground(mut self) -> Self {
658        self.foreground = None;
659        self.base_style = self.base_style.no_foreground();
660        self
661    }
662
663    /// Set background to a theme color slot.
664    pub fn background(mut self, slot: ColorSlot) -> Self {
665        self.background = Some(ThemedColor::Slot(slot));
666        self
667    }
668
669    /// Set background to a fixed color (ignores theme).
670    pub fn background_fixed(mut self, color: impl Into<Color>) -> Self {
671        self.background = Some(ThemedColor::Fixed(color.into()));
672        self
673    }
674
675    /// Set background to a computed theme color.
676    pub fn background_computed(mut self, slot: ColorSlot, transform: ColorTransform) -> Self {
677        self.background = Some(ThemedColor::Computed(slot, transform));
678        self
679    }
680
681    /// Clear any themed background.
682    pub fn no_background(mut self) -> Self {
683        self.background = None;
684        self.base_style = self.base_style.no_background();
685        self
686    }
687
688    /// Set border foreground to a theme color slot.
689    pub fn border_foreground(mut self, slot: ColorSlot) -> Self {
690        self.border_foreground = Some(ThemedColor::Slot(slot));
691        self
692    }
693
694    /// Set border foreground to a fixed color.
695    pub fn border_foreground_fixed(mut self, color: impl Into<Color>) -> Self {
696        self.border_foreground = Some(ThemedColor::Fixed(color.into()));
697        self
698    }
699
700    /// Set border foreground to a computed theme color.
701    pub fn border_foreground_computed(
702        mut self,
703        slot: ColorSlot,
704        transform: ColorTransform,
705    ) -> Self {
706        self.border_foreground = Some(ThemedColor::Computed(slot, transform));
707        self
708    }
709
710    /// Set border background to a theme color slot.
711    pub fn border_background(mut self, slot: ColorSlot) -> Self {
712        self.border_background = Some(ThemedColor::Slot(slot));
713        self
714    }
715
716    /// Set border background to a fixed color.
717    pub fn border_background_fixed(mut self, color: impl Into<Color>) -> Self {
718        self.border_background = Some(ThemedColor::Fixed(color.into()));
719        self
720    }
721
722    /// Set border background to a computed theme color.
723    pub fn border_background_computed(
724        mut self,
725        slot: ColorSlot,
726        transform: ColorTransform,
727    ) -> Self {
728        self.border_background = Some(ThemedColor::Computed(slot, transform));
729        self
730    }
731
732    // ---------------------------------------------------------------------
733    // Delegated non-color style methods
734    // ---------------------------------------------------------------------
735
736    /// Set the underlying string value for this style.
737    pub fn set_string(mut self, s: impl Into<String>) -> Self {
738        self.base_style = self.base_style.set_string(s);
739        self
740    }
741
742    /// Get the underlying string value.
743    pub fn value(&self) -> &str {
744        self.base_style.value()
745    }
746
747    /// Enable bold text.
748    pub fn bold(mut self) -> Self {
749        self.base_style = self.base_style.bold();
750        self
751    }
752
753    /// Enable italic text.
754    pub fn italic(mut self) -> Self {
755        self.base_style = self.base_style.italic();
756        self
757    }
758
759    /// Enable underline text.
760    pub fn underline(mut self) -> Self {
761        self.base_style = self.base_style.underline();
762        self
763    }
764
765    /// Enable strikethrough text.
766    pub fn strikethrough(mut self) -> Self {
767        self.base_style = self.base_style.strikethrough();
768        self
769    }
770
771    /// Enable reverse video.
772    pub fn reverse(mut self) -> Self {
773        self.base_style = self.base_style.reverse();
774        self
775    }
776
777    /// Enable blinking.
778    pub fn blink(mut self) -> Self {
779        self.base_style = self.base_style.blink();
780        self
781    }
782
783    /// Enable faint text.
784    pub fn faint(mut self) -> Self {
785        self.base_style = self.base_style.faint();
786        self
787    }
788
789    /// Toggle underline spaces.
790    pub fn underline_spaces(mut self, v: bool) -> Self {
791        self.base_style = self.base_style.underline_spaces(v);
792        self
793    }
794
795    /// Toggle strikethrough spaces.
796    pub fn strikethrough_spaces(mut self, v: bool) -> Self {
797        self.base_style = self.base_style.strikethrough_spaces(v);
798        self
799    }
800
801    /// Set fixed width.
802    pub fn width(mut self, w: u16) -> Self {
803        self.base_style = self.base_style.width(w);
804        self
805    }
806
807    /// Set fixed height.
808    pub fn height(mut self, h: u16) -> Self {
809        self.base_style = self.base_style.height(h);
810        self
811    }
812
813    /// Set maximum width.
814    pub fn max_width(mut self, w: u16) -> Self {
815        self.base_style = self.base_style.max_width(w);
816        self
817    }
818
819    /// Set maximum height.
820    pub fn max_height(mut self, h: u16) -> Self {
821        self.base_style = self.base_style.max_height(h);
822        self
823    }
824
825    /// Set both horizontal and vertical alignment.
826    pub fn align(mut self, p: Position) -> Self {
827        self.base_style = self.base_style.align(p);
828        self
829    }
830
831    /// Set horizontal alignment.
832    pub fn align_horizontal(mut self, p: Position) -> Self {
833        self.base_style = self.base_style.align_horizontal(p);
834        self
835    }
836
837    /// Set vertical alignment.
838    pub fn align_vertical(mut self, p: Position) -> Self {
839        self.base_style = self.base_style.align_vertical(p);
840        self
841    }
842
843    /// Set padding.
844    pub fn padding(mut self, sides: impl Into<Sides<u16>>) -> Self {
845        self.base_style = self.base_style.padding(sides);
846        self
847    }
848
849    /// Set padding top.
850    pub fn padding_top(mut self, n: u16) -> Self {
851        self.base_style = self.base_style.padding_top(n);
852        self
853    }
854
855    /// Set padding right.
856    pub fn padding_right(mut self, n: u16) -> Self {
857        self.base_style = self.base_style.padding_right(n);
858        self
859    }
860
861    /// Set padding bottom.
862    pub fn padding_bottom(mut self, n: u16) -> Self {
863        self.base_style = self.base_style.padding_bottom(n);
864        self
865    }
866
867    /// Set padding left.
868    pub fn padding_left(mut self, n: u16) -> Self {
869        self.base_style = self.base_style.padding_left(n);
870        self
871    }
872
873    /// Set margin.
874    pub fn margin(mut self, sides: impl Into<Sides<u16>>) -> Self {
875        self.base_style = self.base_style.margin(sides);
876        self
877    }
878
879    /// Set margin top.
880    pub fn margin_top(mut self, n: u16) -> Self {
881        self.base_style = self.base_style.margin_top(n);
882        self
883    }
884
885    /// Set margin right.
886    pub fn margin_right(mut self, n: u16) -> Self {
887        self.base_style = self.base_style.margin_right(n);
888        self
889    }
890
891    /// Set margin bottom.
892    pub fn margin_bottom(mut self, n: u16) -> Self {
893        self.base_style = self.base_style.margin_bottom(n);
894        self
895    }
896
897    /// Set margin left.
898    pub fn margin_left(mut self, n: u16) -> Self {
899        self.base_style = self.base_style.margin_left(n);
900        self
901    }
902
903    /// Set margin background (fixed color).
904    pub fn margin_background(mut self, color: impl Into<String>) -> Self {
905        self.base_style = self.base_style.margin_background(color);
906        self
907    }
908
909    /// Set border style.
910    pub fn border(mut self, border: Border) -> Self {
911        self.base_style = self.base_style.border(border);
912        self
913    }
914
915    /// Set border style (alias).
916    pub fn border_style(mut self, border: Border) -> Self {
917        self.base_style = self.base_style.border_style(border);
918        self
919    }
920
921    /// Enable or disable top border.
922    pub fn border_top(mut self, v: bool) -> Self {
923        self.base_style = self.base_style.border_top(v);
924        self
925    }
926
927    /// Enable or disable right border.
928    pub fn border_right(mut self, v: bool) -> Self {
929        self.base_style = self.base_style.border_right(v);
930        self
931    }
932
933    /// Enable or disable bottom border.
934    pub fn border_bottom(mut self, v: bool) -> Self {
935        self.base_style = self.base_style.border_bottom(v);
936        self
937    }
938
939    /// Enable or disable left border.
940    pub fn border_left(mut self, v: bool) -> Self {
941        self.base_style = self.base_style.border_left(v);
942        self
943    }
944
945    /// Inline mode.
946    pub fn inline(mut self) -> Self {
947        self.base_style = self.base_style.inline();
948        self
949    }
950
951    /// Set tab width.
952    pub fn tab_width(mut self, n: i8) -> Self {
953        self.base_style = self.base_style.tab_width(n);
954        self
955    }
956
957    /// Apply a transform function to the rendered string.
958    pub fn transform<F>(mut self, f: F) -> Self
959    where
960        F: Fn(&str) -> String + Send + Sync + 'static,
961    {
962        self.base_style = self.base_style.transform(f);
963        self
964    }
965
966    /// Set the renderer.
967    pub fn renderer(mut self, r: Arc<Renderer>) -> Self {
968        self.base_style = self.base_style.renderer(r);
969        self
970    }
971
972    /// Check if a property is set on the base style.
973    pub fn is_set(&self, prop: crate::style::Props) -> bool {
974        self.base_style.is_set(prop)
975    }
976}
977
978#[allow(clippy::many_single_char_names)]
979impl ColorTransform {
980    fn apply(self, color: Color) -> Color {
981        let (r, g, b) = if let Some((r, g, b)) = color.as_rgb() {
982            (r, g, b)
983        } else if let Some(n) = color.as_ansi() {
984            ansi256_to_rgb(n)
985        } else {
986            return color;
987        };
988
989        let (h, mut s, mut l) = rgb_to_hsl(r, g, b);
990        let amount = |v: f32| v.clamp(0.0, 1.0);
991
992        match self {
993            ColorTransform::Lighten(a) => l = (l + amount(a)).min(1.0),
994            ColorTransform::Darken(a) => l = (l - amount(a)).max(0.0),
995            ColorTransform::Saturate(a) => s = (s + amount(a)).min(1.0),
996            ColorTransform::Desaturate(a) => s = (s - amount(a)).max(0.0),
997            ColorTransform::Alpha(a) => {
998                let a = amount(a);
999                l = (l * a).min(1.0);
1000            }
1001        }
1002
1003        let (nr, ng, nb) = hsl_to_rgb(h, s, l);
1004        Color::from(format!("#{:02x}{:02x}{:02x}", nr, ng, nb))
1005    }
1006}
1007
1008#[allow(clippy::many_single_char_names)]
1009fn rgb_to_hsl(r: u8, g: u8, b: u8) -> (f32, f32, f32) {
1010    let r = r as f32 / 255.0;
1011    let g = g as f32 / 255.0;
1012    let b = b as f32 / 255.0;
1013
1014    let max = r.max(g).max(b);
1015    let min = r.min(g).min(b);
1016    let l = f32::midpoint(max, min);
1017
1018    if (max - min).abs() < f32::EPSILON {
1019        return (0.0, 0.0, l);
1020    }
1021
1022    let d = max - min;
1023    let s = if l > 0.5 {
1024        d / (2.0 - max - min)
1025    } else {
1026        d / (max + min)
1027    };
1028
1029    let mut h = if (max - r).abs() < f32::EPSILON {
1030        (g - b) / d + if g < b { 6.0 } else { 0.0 }
1031    } else if (max - g).abs() < f32::EPSILON {
1032        (b - r) / d + 2.0
1033    } else {
1034        (r - g) / d + 4.0
1035    };
1036
1037    h /= 6.0;
1038    (h * 360.0, s, l)
1039}
1040
1041#[allow(clippy::many_single_char_names, clippy::suboptimal_flops)]
1042fn hsl_to_rgb(h: f32, s: f32, l: f32) -> (u8, u8, u8) {
1043    if s == 0.0 {
1044        let v = (l * 255.0).round() as u8;
1045        return (v, v, v);
1046    }
1047
1048    let h = h / 360.0;
1049    let q = if l < 0.5 {
1050        l * (1.0 + s)
1051    } else {
1052        l + s - l * s
1053    };
1054    let p = 2.0 * l - q;
1055
1056    fn hue_to_rgb(p: f32, q: f32, mut t: f32) -> f32 {
1057        if t < 0.0 {
1058            t += 1.0;
1059        }
1060        if t > 1.0 {
1061            t -= 1.0;
1062        }
1063        if t < 1.0 / 6.0 {
1064            return p + (q - p) * 6.0 * t;
1065        }
1066        if t < 1.0 / 2.0 {
1067            return q;
1068        }
1069        if t < 2.0 / 3.0 {
1070            return p + (q - p) * (2.0 / 3.0 - t) * 6.0;
1071        }
1072        p
1073    }
1074
1075    let r = hue_to_rgb(p, q, h + 1.0 / 3.0);
1076    let g = hue_to_rgb(p, q, h);
1077    let b = hue_to_rgb(p, q, h - 1.0 / 3.0);
1078
1079    (
1080        (r * 255.0).round() as u8,
1081        (g * 255.0).round() as u8,
1082        (b * 255.0).round() as u8,
1083    )
1084}
1085
1086/// Cached themed style that invalidates on theme changes.
1087pub struct CachedThemedStyle {
1088    themed: ThemedStyle,
1089    cache: Arc<RwLock<Option<Style>>>,
1090    listener_id: ListenerId,
1091}
1092
1093impl CachedThemedStyle {
1094    /// Create a cached themed style.
1095    pub fn new(themed: ThemedStyle) -> Self {
1096        let cache = Arc::new(RwLock::new(None));
1097        let cache_ref = Arc::clone(&cache);
1098        let listener_id = themed.context.on_change(move |_theme| {
1099            if let Ok(mut guard) = cache_ref.write() {
1100                *guard = None;
1101            }
1102            trace!("cached_themed_style cache invalidated");
1103        });
1104
1105        Self {
1106            themed,
1107            cache,
1108            listener_id,
1109        }
1110    }
1111
1112    /// Resolve with caching.
1113    pub fn resolve(&self) -> Style {
1114        if let Ok(cache) = self.cache.read() {
1115            if let Some(style) = cache.as_ref() {
1116                trace!("cached_themed_style cache hit");
1117                return style.clone();
1118            }
1119        }
1120
1121        trace!("cached_themed_style cache miss");
1122        let resolved = self.themed.resolve();
1123        if let Ok(mut cache) = self.cache.write() {
1124            *cache = Some(resolved.clone());
1125        }
1126        resolved
1127    }
1128
1129    /// Render text using the cached themed style.
1130    pub fn render(&self, text: &str) -> String {
1131        self.resolve().render(text)
1132    }
1133
1134    /// Manually invalidate the cache.
1135    pub fn invalidate(&self) {
1136        if let Ok(mut cache) = self.cache.write() {
1137            *cache = None;
1138        }
1139    }
1140}
1141
1142impl Drop for CachedThemedStyle {
1143    fn drop(&mut self) {
1144        self.themed.context.remove_listener(self.listener_id);
1145    }
1146}
1147
1148/// Async theme context backed by a tokio watch channel.
1149#[cfg(feature = "tokio")]
1150pub struct AsyncThemeContext {
1151    sender: tokio::sync::watch::Sender<Theme>,
1152    receiver: tokio::sync::watch::Receiver<Theme>,
1153}
1154
1155#[cfg(feature = "tokio")]
1156impl AsyncThemeContext {
1157    /// Create a new async context with the provided theme.
1158    pub fn new(initial: Theme) -> Self {
1159        let (sender, receiver) = tokio::sync::watch::channel(initial);
1160        Self { sender, receiver }
1161    }
1162
1163    /// Create a new async context from a preset.
1164    pub fn from_preset(preset: ThemePreset) -> Self {
1165        Self::new(preset.to_theme())
1166    }
1167
1168    /// Returns the current theme snapshot.
1169    pub fn current(&self) -> Theme {
1170        self.receiver.borrow().clone()
1171    }
1172
1173    /// Switch to a new theme.
1174    pub fn set_theme(&self, theme: Theme) {
1175        let from = self.receiver.borrow().name().to_string();
1176        let to = theme.name().to_string();
1177        let _ = self.sender.send(theme);
1178        info!(theme.from = %from, theme.to = %to, "Theme switched (async)");
1179    }
1180
1181    /// Switch to a preset theme.
1182    pub fn set_preset(&self, preset: ThemePreset) {
1183        self.set_theme(preset.to_theme());
1184    }
1185
1186    /// Subscribe to theme changes.
1187    pub fn subscribe(&self) -> tokio::sync::watch::Receiver<Theme> {
1188        self.receiver.clone()
1189    }
1190
1191    /// Await the next theme change.
1192    ///
1193    /// # Errors
1194    ///
1195    /// Returns an error if the sender has been dropped.
1196    pub async fn changed(&mut self) -> Result<(), tokio::sync::watch::error::RecvError> {
1197        self.receiver.changed().await
1198    }
1199}
1200
1201impl Theme {
1202    /// Creates a new theme with the given name, dark mode flag, and colors.
1203    pub fn new(name: impl Into<String>, is_dark: bool, colors: ThemeColors) -> Self {
1204        let meta = ThemeMeta {
1205            variant: Some(if is_dark {
1206                ThemeVariant::Dark
1207            } else {
1208                ThemeVariant::Light
1209            }),
1210            ..ThemeMeta::default()
1211        };
1212        Self {
1213            name: name.into(),
1214            is_dark,
1215            colors,
1216            description: None,
1217            author: None,
1218            meta,
1219        }
1220    }
1221
1222    /// Returns the theme name.
1223    pub fn name(&self) -> &str {
1224        &self.name
1225    }
1226
1227    /// Returns true if this is a dark theme.
1228    pub fn is_dark(&self) -> bool {
1229        self.is_dark
1230    }
1231
1232    /// Returns the optional theme description.
1233    pub fn description(&self) -> Option<&str> {
1234        self.description.as_deref()
1235    }
1236
1237    /// Returns the optional theme author.
1238    pub fn author(&self) -> Option<&str> {
1239        self.author.as_deref()
1240    }
1241
1242    /// Returns the theme metadata.
1243    pub fn meta(&self) -> &ThemeMeta {
1244        &self.meta
1245    }
1246
1247    /// Returns a mutable reference to the theme metadata.
1248    pub fn meta_mut(&mut self) -> &mut ThemeMeta {
1249        &mut self.meta
1250    }
1251
1252    /// Returns the theme's color palette.
1253    pub fn colors(&self) -> &ThemeColors {
1254        &self.colors
1255    }
1256
1257    /// Returns a mutable reference to the theme's color palette.
1258    pub fn colors_mut(&mut self) -> &mut ThemeColors {
1259        &mut self.colors
1260    }
1261
1262    /// Returns the color for the given slot.
1263    pub fn get(&self, slot: ColorSlot) -> Color {
1264        self.colors.get(slot).clone()
1265    }
1266
1267    /// Creates a new Style configured to use this theme.
1268    ///
1269    /// The returned style has no properties set but is configured to use
1270    /// this theme's renderer settings.
1271    pub fn style(&self) -> Style {
1272        Style::new()
1273    }
1274
1275    // ========================
1276    // Builder Methods
1277    // ========================
1278
1279    /// Sets the theme name.
1280    pub fn with_name(mut self, name: impl Into<String>) -> Self {
1281        self.name = name.into();
1282        self
1283    }
1284
1285    /// Sets whether this is a dark theme.
1286    pub fn with_dark(mut self, is_dark: bool) -> Self {
1287        self.is_dark = is_dark;
1288        self.meta.variant = Some(if is_dark {
1289            ThemeVariant::Dark
1290        } else {
1291            ThemeVariant::Light
1292        });
1293        self
1294    }
1295
1296    /// Replaces the color palette.
1297    pub fn with_colors(mut self, colors: ThemeColors) -> Self {
1298        self.colors = colors;
1299        self
1300    }
1301
1302    /// Sets the theme description.
1303    pub fn with_description(mut self, description: impl Into<String>) -> Self {
1304        self.description = Some(description.into());
1305        self
1306    }
1307
1308    /// Sets the theme author.
1309    pub fn with_author(mut self, author: impl Into<String>) -> Self {
1310        self.author = Some(author.into());
1311        self
1312    }
1313
1314    /// Replaces theme metadata.
1315    pub fn with_meta(mut self, meta: ThemeMeta) -> Self {
1316        self.meta = meta;
1317        self
1318    }
1319
1320    /// Validate that this theme has usable color values.
1321    ///
1322    /// # Errors
1323    /// Returns `ThemeValidationError` if any color slot is empty or invalid.
1324    pub fn validate(&self) -> Result<(), ThemeValidationError> {
1325        self.colors.validate()?;
1326        let _ = self.check_contrast_aa(ColorSlot::Foreground, ColorSlot::Background);
1327        Ok(())
1328    }
1329
1330    /// Calculate contrast ratio between two theme slots.
1331    pub fn contrast_ratio(&self, fg: ColorSlot, bg: ColorSlot) -> f64 {
1332        let fg_lum = self.get(fg).relative_luminance();
1333        let bg_lum = self.get(bg).relative_luminance();
1334        let lighter = fg_lum.max(bg_lum);
1335        let darker = fg_lum.min(bg_lum);
1336        (lighter + 0.05) / (darker + 0.05)
1337    }
1338
1339    /// Check if a slot combination meets WCAG AA contrast (>= 4.5:1).
1340    pub fn check_contrast_aa(&self, fg: ColorSlot, bg: ColorSlot) -> bool {
1341        let ratio = self.contrast_ratio(fg, bg);
1342        let ok = ratio >= 4.5;
1343        if !ok {
1344            warn!(
1345                theme.contrast_ratio = ratio,
1346                theme.fg = ?fg,
1347                theme.bg = ?bg,
1348                theme.name = %self.name(),
1349                "Theme contrast below WCAG AA"
1350            );
1351        }
1352        ok
1353    }
1354
1355    /// Check if a slot combination meets WCAG AAA contrast (>= 7.0:1).
1356    pub fn check_contrast_aaa(&self, fg: ColorSlot, bg: ColorSlot) -> bool {
1357        self.contrast_ratio(fg, bg) >= 7.0
1358    }
1359
1360    fn normalize(&mut self) {
1361        if let Some(variant) = self.meta.variant {
1362            self.is_dark = matches!(variant, ThemeVariant::Dark);
1363        } else {
1364            self.meta.variant = Some(if self.is_dark {
1365                ThemeVariant::Dark
1366            } else {
1367                ThemeVariant::Light
1368            });
1369        }
1370    }
1371
1372    /// Load a theme from JSON text.
1373    ///
1374    /// # Errors
1375    /// Returns `ThemeLoadError` if JSON parsing or validation fails.
1376    pub fn from_json(json: &str) -> Result<Self, ThemeLoadError> {
1377        let mut theme: Theme = serde_json::from_str(json)?;
1378        theme.normalize();
1379        theme.validate()?;
1380        Ok(theme)
1381    }
1382
1383    /// Load a theme from TOML text.
1384    ///
1385    /// # Errors
1386    /// Returns `ThemeLoadError` if TOML parsing or validation fails.
1387    pub fn from_toml(toml: &str) -> Result<Self, ThemeLoadError> {
1388        let mut theme: Theme = toml::from_str(toml)?;
1389        theme.normalize();
1390        theme.validate()?;
1391        Ok(theme)
1392    }
1393
1394    /// Load a theme from YAML text.
1395    ///
1396    /// # Errors
1397    /// Returns `ThemeLoadError` if YAML parsing or validation fails.
1398    #[cfg(feature = "yaml")]
1399    pub fn from_yaml(yaml: &str) -> Result<Self, ThemeLoadError> {
1400        let mut theme: Theme = serde_yaml::from_str(yaml)?;
1401        theme.normalize();
1402        theme.validate()?;
1403        Ok(theme)
1404    }
1405
1406    /// Load a theme from a file (format inferred by extension).
1407    ///
1408    /// # Errors
1409    /// Returns `ThemeLoadError` if reading, parsing, or validation fails.
1410    ///
1411    /// # Availability
1412    /// This method is only available with the `native` feature (not on WASM).
1413    #[cfg(feature = "native")]
1414    pub fn from_file(path: impl AsRef<Path>) -> Result<Self, ThemeLoadError> {
1415        let path = path.as_ref();
1416        let content = fs::read_to_string(path)?;
1417        match path.extension().and_then(|e| e.to_str()) {
1418            Some("json") => Self::from_json(&content),
1419            Some("toml") => Self::from_toml(&content),
1420            Some("yaml" | "yml") => {
1421                #[cfg(feature = "yaml")]
1422                {
1423                    Self::from_yaml(&content)
1424                }
1425                #[cfg(not(feature = "yaml"))]
1426                {
1427                    Err(ThemeLoadError::UnsupportedFormat("yaml".into()))
1428                }
1429            }
1430            Some(ext) => Err(ThemeLoadError::UnsupportedFormat(ext.into())),
1431            None => Err(ThemeLoadError::UnsupportedFormat("unknown".into())),
1432        }
1433    }
1434
1435    /// Serialize this theme to JSON.
1436    ///
1437    /// # Errors
1438    /// Returns `ThemeSaveError` if serialization fails.
1439    pub fn to_json(&self) -> Result<String, ThemeSaveError> {
1440        serde_json::to_string_pretty(self).map_err(ThemeSaveError::Json)
1441    }
1442
1443    /// Serialize this theme to TOML.
1444    ///
1445    /// # Errors
1446    /// Returns `ThemeSaveError` if serialization fails.
1447    pub fn to_toml(&self) -> Result<String, ThemeSaveError> {
1448        toml::to_string_pretty(self).map_err(ThemeSaveError::Toml)
1449    }
1450
1451    /// Serialize this theme to YAML.
1452    ///
1453    /// # Errors
1454    /// Returns `ThemeSaveError` if serialization fails.
1455    #[cfg(feature = "yaml")]
1456    pub fn to_yaml(&self) -> Result<String, ThemeSaveError> {
1457        serde_yaml::to_string(self).map_err(ThemeSaveError::Yaml)
1458    }
1459
1460    /// Save this theme to a file (format inferred by extension).
1461    ///
1462    /// # Errors
1463    /// Returns `ThemeSaveError` if serialization or writing fails.
1464    ///
1465    /// # Availability
1466    /// This method is only available with the `native` feature (not on WASM).
1467    #[cfg(feature = "native")]
1468    pub fn to_file(&self, path: impl AsRef<Path>) -> Result<(), ThemeSaveError> {
1469        let path = path.as_ref();
1470        let content = match path.extension().and_then(|e| e.to_str()) {
1471            Some("json") | None => self.to_json()?,
1472            Some("toml") => self.to_toml()?,
1473            Some("yaml" | "yml") => {
1474                #[cfg(feature = "yaml")]
1475                {
1476                    self.to_yaml()?
1477                }
1478                #[cfg(not(feature = "yaml"))]
1479                {
1480                    return Err(ThemeSaveError::UnsupportedFormat("yaml".into()));
1481                }
1482            }
1483            Some(ext) => return Err(ThemeSaveError::UnsupportedFormat(ext.into())),
1484        };
1485
1486        fs::write(path, content).map_err(ThemeSaveError::Io)
1487    }
1488
1489    // ========================
1490    // Default Themes
1491    // ========================
1492
1493    /// Returns the default dark theme.
1494    ///
1495    /// This theme uses colors suitable for dark terminal backgrounds.
1496    pub fn dark() -> Self {
1497        Self::new("Dark", true, ThemeColors::dark())
1498    }
1499
1500    /// Returns the default light theme.
1501    ///
1502    /// This theme uses colors suitable for light terminal backgrounds.
1503    pub fn light() -> Self {
1504        Self::new("Light", false, ThemeColors::light())
1505    }
1506
1507    /// Returns the Dracula theme.
1508    ///
1509    /// A popular dark theme with purple accents.
1510    /// <https://draculatheme.com>
1511    pub fn dracula() -> Self {
1512        Self::new("Dracula", true, ThemeColors::dracula())
1513    }
1514
1515    /// Returns the Nord theme.
1516    ///
1517    /// An arctic, north-bluish color palette.
1518    /// <https://www.nordtheme.com>
1519    pub fn nord() -> Self {
1520        Self::new("Nord", true, ThemeColors::nord())
1521    }
1522
1523    /// Returns a Catppuccin theme for the requested flavor.
1524    ///
1525    /// A soothing pastel theme with warm tones.
1526    /// <https://catppuccin.com>
1527    pub fn catppuccin(flavor: CatppuccinFlavor) -> Self {
1528        match flavor {
1529            CatppuccinFlavor::Latte => {
1530                Self::new("Catppuccin Latte", false, ThemeColors::catppuccin_latte())
1531            }
1532            CatppuccinFlavor::Frappe => {
1533                Self::new("Catppuccin Frappe", true, ThemeColors::catppuccin_frappe())
1534            }
1535            CatppuccinFlavor::Macchiato => Self::new(
1536                "Catppuccin Macchiato",
1537                true,
1538                ThemeColors::catppuccin_macchiato(),
1539            ),
1540            CatppuccinFlavor::Mocha => {
1541                Self::new("Catppuccin Mocha", true, ThemeColors::catppuccin_mocha())
1542            }
1543        }
1544    }
1545
1546    /// Returns the Catppuccin Latte theme.
1547    pub fn catppuccin_latte() -> Self {
1548        Self::catppuccin(CatppuccinFlavor::Latte)
1549    }
1550
1551    /// Returns the Catppuccin Frappe theme.
1552    pub fn catppuccin_frappe() -> Self {
1553        Self::catppuccin(CatppuccinFlavor::Frappe)
1554    }
1555
1556    /// Returns the Catppuccin Macchiato theme.
1557    pub fn catppuccin_macchiato() -> Self {
1558        Self::catppuccin(CatppuccinFlavor::Macchiato)
1559    }
1560
1561    /// Returns the Catppuccin Mocha theme.
1562    pub fn catppuccin_mocha() -> Self {
1563        Self::catppuccin(CatppuccinFlavor::Mocha)
1564    }
1565}
1566
1567impl Default for Theme {
1568    fn default() -> Self {
1569        Self::dark()
1570    }
1571}
1572
1573impl ThemePreset {
1574    /// Convert this preset into a concrete theme instance.
1575    pub fn to_theme(&self) -> Theme {
1576        let theme = match *self {
1577            ThemePreset::Dark => Theme::dark(),
1578            ThemePreset::Light => Theme::light(),
1579            ThemePreset::Dracula => Theme::dracula(),
1580            ThemePreset::Nord => Theme::nord(),
1581            ThemePreset::Catppuccin(flavor) => Theme::catppuccin(flavor),
1582        };
1583
1584        info!(theme.preset = %self, theme.name = %theme.name(), "Loaded theme preset");
1585        theme
1586    }
1587}
1588
1589impl ThemeColors {
1590    /// Returns the color for the given slot.
1591    pub fn get(&self, slot: ColorSlot) -> &Color {
1592        let color = match slot {
1593            ColorSlot::Primary => &self.primary,
1594            ColorSlot::Secondary => &self.secondary,
1595            ColorSlot::Accent => &self.accent,
1596            ColorSlot::Background => &self.background,
1597            ColorSlot::Foreground | ColorSlot::Text => &self.text,
1598            ColorSlot::TextMuted => &self.text_muted,
1599            ColorSlot::TextDisabled => &self.text_disabled,
1600            ColorSlot::Surface => &self.surface,
1601            ColorSlot::SurfaceAlt => &self.surface_alt,
1602            ColorSlot::Success => &self.success,
1603            ColorSlot::Warning => &self.warning,
1604            ColorSlot::Error => &self.error,
1605            ColorSlot::Info => &self.info,
1606            ColorSlot::Border => &self.border,
1607            ColorSlot::BorderMuted => &self.border_muted,
1608            ColorSlot::Separator => &self.separator,
1609            ColorSlot::Focus => &self.focus,
1610            ColorSlot::Selection => &self.selection,
1611            ColorSlot::Hover => &self.hover,
1612            ColorSlot::CodeKeyword => &self.code_keyword,
1613            ColorSlot::CodeString => &self.code_string,
1614            ColorSlot::CodeNumber => &self.code_number,
1615            ColorSlot::CodeComment => &self.code_comment,
1616            ColorSlot::CodeFunction => &self.code_function,
1617            ColorSlot::CodeType => &self.code_type,
1618            ColorSlot::CodeVariable => &self.code_variable,
1619            ColorSlot::CodeOperator => &self.code_operator,
1620        };
1621
1622        debug!(theme.slot = ?slot, theme.value = %color.0, "Theme color lookup");
1623        color
1624    }
1625
1626    /// Returns the custom color slots.
1627    pub fn custom(&self) -> &HashMap<String, Color> {
1628        &self.custom
1629    }
1630
1631    /// Returns a mutable reference to the custom color slots.
1632    pub fn custom_mut(&mut self) -> &mut HashMap<String, Color> {
1633        &mut self.custom
1634    }
1635
1636    /// Returns a custom color by name.
1637    pub fn get_custom(&self, name: &str) -> Option<&Color> {
1638        self.custom.get(name)
1639    }
1640
1641    /// Validate that all color slots are usable.
1642    ///
1643    /// # Errors
1644    /// Returns `ThemeValidationError` if any color slot is empty or invalid.
1645    pub fn validate(&self) -> Result<(), ThemeValidationError> {
1646        fn validate_color(slot: &'static str, color: &Color) -> Result<(), ThemeValidationError> {
1647            if color.0.trim().is_empty() {
1648                return Err(ThemeValidationError::EmptyColor(slot));
1649            }
1650            if !color.is_valid() {
1651                return Err(ThemeValidationError::InvalidColor {
1652                    slot,
1653                    value: color.0.clone(),
1654                });
1655            }
1656            Ok(())
1657        }
1658
1659        validate_color("primary", &self.primary)?;
1660        validate_color("secondary", &self.secondary)?;
1661        validate_color("accent", &self.accent)?;
1662        validate_color("background", &self.background)?;
1663        validate_color("surface", &self.surface)?;
1664        validate_color("surface_alt", &self.surface_alt)?;
1665        validate_color("text", &self.text)?;
1666        validate_color("text_muted", &self.text_muted)?;
1667        validate_color("text_disabled", &self.text_disabled)?;
1668        validate_color("success", &self.success)?;
1669        validate_color("warning", &self.warning)?;
1670        validate_color("error", &self.error)?;
1671        validate_color("info", &self.info)?;
1672        validate_color("border", &self.border)?;
1673        validate_color("border_muted", &self.border_muted)?;
1674        validate_color("separator", &self.separator)?;
1675        validate_color("focus", &self.focus)?;
1676        validate_color("selection", &self.selection)?;
1677        validate_color("hover", &self.hover)?;
1678        validate_color("code_keyword", &self.code_keyword)?;
1679        validate_color("code_string", &self.code_string)?;
1680        validate_color("code_number", &self.code_number)?;
1681        validate_color("code_comment", &self.code_comment)?;
1682        validate_color("code_function", &self.code_function)?;
1683        validate_color("code_type", &self.code_type)?;
1684        validate_color("code_variable", &self.code_variable)?;
1685        validate_color("code_operator", &self.code_operator)?;
1686
1687        for (name, color) in &self.custom {
1688            if name.trim().is_empty() {
1689                return Err(ThemeValidationError::InvalidCustomName);
1690            }
1691            if !color.is_valid() {
1692                return Err(ThemeValidationError::InvalidCustomColor {
1693                    name: name.clone(),
1694                    value: color.0.clone(),
1695                });
1696            }
1697        }
1698
1699        Ok(())
1700    }
1701
1702    /// Creates a new `ThemeColors` with all slots set to the same color.
1703    ///
1704    /// Useful as a starting point for building custom themes.
1705    pub fn uniform(color: impl Into<Color>) -> Self {
1706        let c = color.into();
1707        Self {
1708            primary: c.clone(),
1709            secondary: c.clone(),
1710            accent: c.clone(),
1711            background: c.clone(),
1712            surface: c.clone(),
1713            surface_alt: c.clone(),
1714            text: c.clone(),
1715            text_muted: c.clone(),
1716            text_disabled: c.clone(),
1717            success: c.clone(),
1718            warning: c.clone(),
1719            error: c.clone(),
1720            info: c.clone(),
1721            border: c.clone(),
1722            border_muted: c.clone(),
1723            separator: c.clone(),
1724            focus: c.clone(),
1725            selection: c.clone(),
1726            hover: c.clone(),
1727            code_keyword: c.clone(),
1728            code_string: c.clone(),
1729            code_number: c.clone(),
1730            code_comment: c.clone(),
1731            code_function: c.clone(),
1732            code_type: c.clone(),
1733            code_variable: c.clone(),
1734            code_operator: c,
1735            custom: HashMap::new(),
1736        }
1737    }
1738
1739    /// Returns the default dark color palette.
1740    pub fn dark() -> Self {
1741        Self {
1742            // Primary palette
1743            primary: Color::from("#7c3aed"),   // Violet
1744            secondary: Color::from("#6366f1"), // Indigo
1745            accent: Color::from("#22d3ee"),    // Cyan
1746
1747            // Backgrounds
1748            background: Color::from("#0f0f0f"),  // Near black
1749            surface: Color::from("#1a1a1a"),     // Dark gray
1750            surface_alt: Color::from("#262626"), // Slightly lighter
1751
1752            // Text
1753            text: Color::from("#fafafa"),          // Near white
1754            text_muted: Color::from("#a1a1aa"),    // Gray
1755            text_disabled: Color::from("#52525b"), // Darker gray
1756
1757            // Semantic
1758            success: Color::from("#22c55e"), // Green
1759            warning: Color::from("#f59e0b"), // Amber
1760            error: Color::from("#ef4444"),   // Red
1761            info: Color::from("#3b82f6"),    // Blue
1762
1763            // UI elements
1764            border: Color::from("#3f3f46"),       // Zinc-700
1765            border_muted: Color::from("#27272a"), // Zinc-800
1766            separator: Color::from("#27272a"),    // Same as border_muted
1767
1768            // Interactive
1769            focus: Color::from("#7c3aed"),     // Same as primary
1770            selection: Color::from("#4c1d95"), // Dark violet
1771            hover: Color::from("#27272a"),     // Subtle highlight
1772
1773            // Code/syntax (based on popular dark themes)
1774            code_keyword: Color::from("#c678dd"),  // Purple
1775            code_string: Color::from("#98c379"),   // Green
1776            code_number: Color::from("#d19a66"),   // Orange
1777            code_comment: Color::from("#5c6370"),  // Gray
1778            code_function: Color::from("#61afef"), // Blue
1779            code_type: Color::from("#e5c07b"),     // Yellow
1780            code_variable: Color::from("#e06c75"), // Red/pink
1781            code_operator: Color::from("#56b6c2"), // Cyan
1782            custom: HashMap::new(),
1783        }
1784    }
1785
1786    /// Returns the default light color palette.
1787    pub fn light() -> Self {
1788        Self {
1789            // Primary palette
1790            primary: Color::from("#7c3aed"),   // Violet
1791            secondary: Color::from("#4f46e5"), // Indigo
1792            accent: Color::from("#0891b2"),    // Cyan (darker for light bg)
1793
1794            // Backgrounds
1795            background: Color::from("#ffffff"),  // White
1796            surface: Color::from("#f4f4f5"),     // Zinc-100
1797            surface_alt: Color::from("#e4e4e7"), // Zinc-200
1798
1799            // Text
1800            text: Color::from("#18181b"),          // Zinc-900
1801            text_muted: Color::from("#71717a"),    // Zinc-500
1802            text_disabled: Color::from("#a1a1aa"), // Zinc-400
1803
1804            // Semantic
1805            success: Color::from("#16a34a"), // Green-600
1806            warning: Color::from("#d97706"), // Amber-600
1807            error: Color::from("#dc2626"),   // Red-600
1808            info: Color::from("#2563eb"),    // Blue-600
1809
1810            // UI elements
1811            border: Color::from("#d4d4d8"),       // Zinc-300
1812            border_muted: Color::from("#e4e4e7"), // Zinc-200
1813            separator: Color::from("#e4e4e7"),    // Same as border_muted
1814
1815            // Interactive
1816            focus: Color::from("#7c3aed"),     // Same as primary
1817            selection: Color::from("#ddd6fe"), // Light violet
1818            hover: Color::from("#f4f4f5"),     // Subtle highlight
1819
1820            // Code/syntax (based on popular light themes)
1821            code_keyword: Color::from("#a626a4"),  // Purple
1822            code_string: Color::from("#50a14f"),   // Green
1823            code_number: Color::from("#986801"),   // Orange/brown
1824            code_comment: Color::from("#a0a1a7"),  // Gray
1825            code_function: Color::from("#4078f2"), // Blue
1826            code_type: Color::from("#c18401"),     // Yellow/gold
1827            code_variable: Color::from("#e45649"), // Red
1828            code_operator: Color::from("#0184bc"), // Cyan
1829            custom: HashMap::new(),
1830        }
1831    }
1832
1833    /// Returns the Dracula color palette.
1834    pub fn dracula() -> Self {
1835        // Dracula theme colors from https://draculatheme.com
1836        Self {
1837            primary: Color::from("#bd93f9"),   // Purple
1838            secondary: Color::from("#ff79c6"), // Pink
1839            accent: Color::from("#8be9fd"),    // Cyan
1840
1841            background: Color::from("#282a36"),  // Background
1842            surface: Color::from("#44475a"),     // Current Line
1843            surface_alt: Color::from("#6272a4"), // Comment
1844
1845            text: Color::from("#f8f8f2"),          // Foreground
1846            text_muted: Color::from("#6272a4"),    // Comment
1847            text_disabled: Color::from("#44475a"), // Current Line
1848
1849            success: Color::from("#50fa7b"), // Green
1850            warning: Color::from("#ffb86c"), // Orange
1851            error: Color::from("#ff5555"),   // Red
1852            info: Color::from("#8be9fd"),    // Cyan
1853
1854            border: Color::from("#44475a"),       // Current Line
1855            border_muted: Color::from("#282a36"), // Background
1856            separator: Color::from("#44475a"),    // Current Line
1857
1858            focus: Color::from("#bd93f9"),     // Purple
1859            selection: Color::from("#44475a"), // Current Line
1860            hover: Color::from("#44475a"),     // Current Line
1861
1862            code_keyword: Color::from("#ff79c6"),  // Pink
1863            code_string: Color::from("#f1fa8c"),   // Yellow
1864            code_number: Color::from("#bd93f9"),   // Purple
1865            code_comment: Color::from("#6272a4"),  // Comment
1866            code_function: Color::from("#50fa7b"), // Green
1867            code_type: Color::from("#8be9fd"),     // Cyan
1868            code_variable: Color::from("#f8f8f2"), // Foreground
1869            code_operator: Color::from("#ff79c6"), // Pink
1870            custom: HashMap::new(),
1871        }
1872    }
1873
1874    /// Returns the Nord color palette.
1875    pub fn nord() -> Self {
1876        // Nord theme colors from https://www.nordtheme.com
1877        Self {
1878            primary: Color::from("#88c0d0"),   // Nord8 (cyan)
1879            secondary: Color::from("#81a1c1"), // Nord9 (blue)
1880            accent: Color::from("#b48ead"),    // Nord15 (purple)
1881
1882            background: Color::from("#2e3440"),  // Nord0
1883            surface: Color::from("#3b4252"),     // Nord1
1884            surface_alt: Color::from("#434c5e"), // Nord2
1885
1886            text: Color::from("#eceff4"),          // Nord6
1887            text_muted: Color::from("#d8dee9"),    // Nord4
1888            text_disabled: Color::from("#4c566a"), // Nord3
1889
1890            success: Color::from("#a3be8c"), // Nord14 (green)
1891            warning: Color::from("#ebcb8b"), // Nord13 (yellow)
1892            error: Color::from("#bf616a"),   // Nord11 (red)
1893            info: Color::from("#5e81ac"),    // Nord10 (blue)
1894
1895            border: Color::from("#4c566a"),       // Nord3
1896            border_muted: Color::from("#3b4252"), // Nord1
1897            separator: Color::from("#3b4252"),    // Nord1
1898
1899            focus: Color::from("#88c0d0"),     // Nord8
1900            selection: Color::from("#434c5e"), // Nord2
1901            hover: Color::from("#3b4252"),     // Nord1
1902
1903            code_keyword: Color::from("#81a1c1"),  // Nord9
1904            code_string: Color::from("#a3be8c"),   // Nord14
1905            code_number: Color::from("#b48ead"),   // Nord15
1906            code_comment: Color::from("#616e88"),  // Muted Nord
1907            code_function: Color::from("#88c0d0"), // Nord8
1908            code_type: Color::from("#8fbcbb"),     // Nord7
1909            code_variable: Color::from("#d8dee9"), // Nord4
1910            code_operator: Color::from("#81a1c1"), // Nord9
1911            custom: HashMap::new(),
1912        }
1913    }
1914
1915    /// Returns the Catppuccin Mocha color palette.
1916    pub fn catppuccin_mocha() -> Self {
1917        // Catppuccin Mocha colors from https://catppuccin.com/palette
1918        Self {
1919            primary: Color::from("#cba6f7"),   // Mauve
1920            secondary: Color::from("#89b4fa"), // Blue
1921            accent: Color::from("#f5c2e7"),    // Pink
1922
1923            background: Color::from("#1e1e2e"),  // Base
1924            surface: Color::from("#313244"),     // Surface0
1925            surface_alt: Color::from("#45475a"), // Surface1
1926
1927            text: Color::from("#cdd6f4"),          // Text
1928            text_muted: Color::from("#a6adc8"),    // Subtext0
1929            text_disabled: Color::from("#6c7086"), // Overlay0
1930
1931            success: Color::from("#a6e3a1"), // Green
1932            warning: Color::from("#f9e2af"), // Yellow
1933            error: Color::from("#f38ba8"),   // Red
1934            info: Color::from("#89dceb"),    // Sky
1935
1936            border: Color::from("#45475a"),       // Surface1
1937            border_muted: Color::from("#313244"), // Surface0
1938            separator: Color::from("#313244"),    // Surface0
1939
1940            focus: Color::from("#cba6f7"),     // Mauve
1941            selection: Color::from("#45475a"), // Surface1
1942            hover: Color::from("#313244"),     // Surface0
1943
1944            code_keyword: Color::from("#cba6f7"),  // Mauve
1945            code_string: Color::from("#a6e3a1"),   // Green
1946            code_number: Color::from("#fab387"),   // Peach
1947            code_comment: Color::from("#6c7086"),  // Overlay0
1948            code_function: Color::from("#89b4fa"), // Blue
1949            code_type: Color::from("#f9e2af"),     // Yellow
1950            code_variable: Color::from("#f5c2e7"), // Pink
1951            code_operator: Color::from("#89dceb"), // Sky
1952            custom: HashMap::new(),
1953        }
1954    }
1955
1956    /// Returns the Catppuccin Latte color palette.
1957    pub fn catppuccin_latte() -> Self {
1958        // Catppuccin Latte colors from https://catppuccin.com/palette
1959        Self {
1960            primary: Color::from("#8839ef"),   // Mauve
1961            secondary: Color::from("#1e66f5"), // Blue
1962            accent: Color::from("#ea76cb"),    // Pink
1963
1964            background: Color::from("#eff1f5"),  // Base
1965            surface: Color::from("#ccd0da"),     // Surface0
1966            surface_alt: Color::from("#bcc0cc"), // Surface1
1967
1968            text: Color::from("#4c4f69"),          // Text
1969            text_muted: Color::from("#6c6f85"),    // Subtext0
1970            text_disabled: Color::from("#9ca0b0"), // Overlay0
1971
1972            success: Color::from("#40a02b"), // Green
1973            warning: Color::from("#df8e1d"), // Yellow
1974            error: Color::from("#d20f39"),   // Red
1975            info: Color::from("#04a5e5"),    // Sky
1976
1977            border: Color::from("#bcc0cc"),       // Surface1
1978            border_muted: Color::from("#ccd0da"), // Surface0
1979            separator: Color::from("#ccd0da"),    // Surface0
1980
1981            focus: Color::from("#8839ef"),     // Mauve
1982            selection: Color::from("#bcc0cc"), // Surface1
1983            hover: Color::from("#ccd0da"),     // Surface0
1984
1985            code_keyword: Color::from("#8839ef"),  // Mauve
1986            code_string: Color::from("#40a02b"),   // Green
1987            code_number: Color::from("#fe640b"),   // Peach
1988            code_comment: Color::from("#9ca0b0"),  // Overlay0
1989            code_function: Color::from("#1e66f5"), // Blue
1990            code_type: Color::from("#df8e1d"),     // Yellow
1991            code_variable: Color::from("#ea76cb"), // Pink
1992            code_operator: Color::from("#04a5e5"), // Sky
1993            custom: HashMap::new(),
1994        }
1995    }
1996
1997    /// Returns the Catppuccin Frappe color palette.
1998    pub fn catppuccin_frappe() -> Self {
1999        // Catppuccin Frappe colors from https://catppuccin.com/palette
2000        Self {
2001            primary: Color::from("#ca9ee6"),   // Mauve
2002            secondary: Color::from("#8caaee"), // Blue
2003            accent: Color::from("#f4b8e4"),    // Pink
2004
2005            background: Color::from("#303446"),  // Base
2006            surface: Color::from("#414559"),     // Surface0
2007            surface_alt: Color::from("#51576d"), // Surface1
2008
2009            text: Color::from("#c6d0f5"),          // Text
2010            text_muted: Color::from("#a5adce"),    // Subtext0
2011            text_disabled: Color::from("#737994"), // Overlay0
2012
2013            success: Color::from("#a6d189"), // Green
2014            warning: Color::from("#e5c890"), // Yellow
2015            error: Color::from("#e78284"),   // Red
2016            info: Color::from("#99d1db"),    // Sky
2017
2018            border: Color::from("#51576d"),       // Surface1
2019            border_muted: Color::from("#414559"), // Surface0
2020            separator: Color::from("#414559"),    // Surface0
2021
2022            focus: Color::from("#ca9ee6"),     // Mauve
2023            selection: Color::from("#51576d"), // Surface1
2024            hover: Color::from("#414559"),     // Surface0
2025
2026            code_keyword: Color::from("#ca9ee6"),  // Mauve
2027            code_string: Color::from("#a6d189"),   // Green
2028            code_number: Color::from("#ef9f76"),   // Peach
2029            code_comment: Color::from("#737994"),  // Overlay0
2030            code_function: Color::from("#8caaee"), // Blue
2031            code_type: Color::from("#e5c890"),     // Yellow
2032            code_variable: Color::from("#f4b8e4"), // Pink
2033            code_operator: Color::from("#99d1db"), // Sky
2034            custom: HashMap::new(),
2035        }
2036    }
2037
2038    /// Returns the Catppuccin Macchiato color palette.
2039    pub fn catppuccin_macchiato() -> Self {
2040        // Catppuccin Macchiato colors from https://catppuccin.com/palette
2041        Self {
2042            primary: Color::from("#c6a0f6"),   // Mauve
2043            secondary: Color::from("#8aadf4"), // Blue
2044            accent: Color::from("#f5bde6"),    // Pink
2045
2046            background: Color::from("#24273a"),  // Base
2047            surface: Color::from("#363a4f"),     // Surface0
2048            surface_alt: Color::from("#494d64"), // Surface1
2049
2050            text: Color::from("#cad3f5"),          // Text
2051            text_muted: Color::from("#a5adcb"),    // Subtext0
2052            text_disabled: Color::from("#6e738d"), // Overlay0
2053
2054            success: Color::from("#a6da95"), // Green
2055            warning: Color::from("#eed49f"), // Yellow
2056            error: Color::from("#ed8796"),   // Red
2057            info: Color::from("#91d7e3"),    // Sky
2058
2059            border: Color::from("#494d64"),       // Surface1
2060            border_muted: Color::from("#363a4f"), // Surface0
2061            separator: Color::from("#363a4f"),    // Surface0
2062
2063            focus: Color::from("#c6a0f6"),     // Mauve
2064            selection: Color::from("#494d64"), // Surface1
2065            hover: Color::from("#363a4f"),     // Surface0
2066
2067            code_keyword: Color::from("#c6a0f6"),  // Mauve
2068            code_string: Color::from("#a6da95"),   // Green
2069            code_number: Color::from("#f5a97f"),   // Peach
2070            code_comment: Color::from("#6e738d"),  // Overlay0
2071            code_function: Color::from("#8aadf4"), // Blue
2072            code_type: Color::from("#eed49f"),     // Yellow
2073            code_variable: Color::from("#f5bde6"), // Pink
2074            code_operator: Color::from("#91d7e3"), // Sky
2075            custom: HashMap::new(),
2076        }
2077    }
2078}
2079
2080impl Default for ThemeColors {
2081    fn default() -> Self {
2082        Self::dark()
2083    }
2084}
2085
2086/// Creates an adaptive color from a theme's light and dark colors.
2087///
2088/// This is useful for creating colors that work correctly in both
2089/// light and dark terminal environments.
2090pub fn adaptive(
2091    light: &ThemeColors,
2092    dark: &ThemeColors,
2093    slot: impl Fn(&ThemeColors) -> &Color,
2094) -> AdaptiveColor {
2095    AdaptiveColor {
2096        light: slot(light).clone(),
2097        dark: slot(dark).clone(),
2098    }
2099}
2100
2101/// Error validating theme colors.
2102#[derive(Error, Debug)]
2103pub enum ThemeValidationError {
2104    #[error("Color slot '{0}' is empty")]
2105    EmptyColor(&'static str),
2106    #[error("Invalid color value '{value}' for slot '{slot}'")]
2107    InvalidColor { slot: &'static str, value: String },
2108    #[error("Custom color name cannot be empty")]
2109    InvalidCustomName,
2110    #[error("Invalid custom color '{value}' for '{name}'")]
2111    InvalidCustomColor { name: String, value: String },
2112}
2113
2114/// Error loading a theme.
2115#[derive(Error, Debug)]
2116pub enum ThemeLoadError {
2117    #[error("JSON error: {0}")]
2118    Json(#[from] serde_json::Error),
2119    #[error("TOML error: {0}")]
2120    Toml(#[from] toml::de::Error),
2121    #[cfg(feature = "yaml")]
2122    #[error("YAML error: {0}")]
2123    Yaml(#[from] serde_yaml::Error),
2124    #[error("IO error: {0}")]
2125    Io(#[from] std::io::Error),
2126    #[error("Unsupported format: {0}")]
2127    UnsupportedFormat(String),
2128    #[error("Validation error: {0}")]
2129    Validation(#[from] ThemeValidationError),
2130}
2131
2132/// Error saving a theme.
2133#[derive(Error, Debug)]
2134pub enum ThemeSaveError {
2135    #[error("JSON error: {0}")]
2136    Json(#[from] serde_json::Error),
2137    #[error("TOML error: {0}")]
2138    Toml(#[from] toml::ser::Error),
2139    #[cfg(feature = "yaml")]
2140    #[error("YAML error: {0}")]
2141    Yaml(#[from] serde_yaml::Error),
2142    #[error("IO error: {0}")]
2143    Io(#[from] std::io::Error),
2144    #[error("Unsupported format: {0}")]
2145    UnsupportedFormat(String),
2146}
2147
2148#[cfg(test)]
2149mod tests {
2150    use super::*;
2151    use crate::renderer::Renderer;
2152    use std::sync::Arc;
2153    use std::sync::atomic::{AtomicUsize, Ordering};
2154
2155    #[test]
2156    fn test_theme_dark_default() {
2157        let theme = Theme::dark();
2158        assert!(theme.is_dark());
2159        assert_eq!(theme.name(), "Dark");
2160    }
2161
2162    #[test]
2163    fn test_theme_light_default() {
2164        let theme = Theme::light();
2165        assert!(!theme.is_dark());
2166        assert_eq!(theme.name(), "Light");
2167    }
2168
2169    #[test]
2170    fn test_theme_dracula() {
2171        let theme = Theme::dracula();
2172        assert!(theme.is_dark());
2173        assert_eq!(theme.name(), "Dracula");
2174        // Dracula's background is #282a36
2175        assert_eq!(theme.colors().background.0, "#282a36");
2176    }
2177
2178    #[test]
2179    fn test_theme_nord() {
2180        let theme = Theme::nord();
2181        assert!(theme.is_dark());
2182        assert_eq!(theme.name(), "Nord");
2183        // Nord's background is #2e3440
2184        assert_eq!(theme.colors().background.0, "#2e3440");
2185    }
2186
2187    #[test]
2188    fn test_theme_catppuccin() {
2189        let theme = Theme::catppuccin_mocha();
2190        assert!(theme.is_dark());
2191        assert_eq!(theme.name(), "Catppuccin Mocha");
2192        // Catppuccin Mocha's background is #1e1e2e
2193        assert_eq!(theme.colors().background.0, "#1e1e2e");
2194    }
2195
2196    #[test]
2197    fn test_theme_catppuccin_latte() {
2198        let theme = Theme::catppuccin_latte();
2199        assert!(!theme.is_dark());
2200        assert_eq!(theme.name(), "Catppuccin Latte");
2201        assert_eq!(theme.colors().background.0, "#eff1f5");
2202        assert_eq!(theme.colors().primary.0, "#8839ef");
2203    }
2204
2205    #[test]
2206    fn test_theme_catppuccin_frappe() {
2207        let theme = Theme::catppuccin_frappe();
2208        assert!(theme.is_dark());
2209        assert_eq!(theme.name(), "Catppuccin Frappe");
2210        assert_eq!(theme.colors().background.0, "#303446");
2211        assert_eq!(theme.colors().primary.0, "#ca9ee6");
2212    }
2213
2214    #[test]
2215    fn test_theme_catppuccin_macchiato() {
2216        let theme = Theme::catppuccin_macchiato();
2217        assert!(theme.is_dark());
2218        assert_eq!(theme.name(), "Catppuccin Macchiato");
2219        assert_eq!(theme.colors().background.0, "#24273a");
2220        assert_eq!(theme.colors().primary.0, "#c6a0f6");
2221    }
2222
2223    #[test]
2224    fn test_theme_preset_to_theme() {
2225        let theme = ThemePreset::Catppuccin(CatppuccinFlavor::Latte).to_theme();
2226        assert_eq!(theme.name(), "Catppuccin Latte");
2227        assert!(!theme.is_dark());
2228    }
2229
2230    #[test]
2231    fn test_theme_contrast_aa() {
2232        let theme = Theme::dark();
2233        assert!(theme.check_contrast_aa(ColorSlot::Foreground, ColorSlot::Background));
2234    }
2235
2236    #[test]
2237    fn test_theme_context_switch() {
2238        let ctx = ThemeContext::from_preset(ThemePreset::Dark);
2239        assert_eq!(ctx.current().name(), "Dark");
2240        ctx.set_preset(ThemePreset::Light);
2241        assert_eq!(ctx.current().name(), "Light");
2242    }
2243
2244    #[test]
2245    fn test_theme_context_listener() {
2246        let ctx = ThemeContext::from_preset(ThemePreset::Dark);
2247        let hits = Arc::new(AtomicUsize::new(0));
2248        let hits_ref = Arc::clone(&hits);
2249        let id = ctx.on_change(move |_theme| {
2250            hits_ref.fetch_add(1, Ordering::SeqCst);
2251        });
2252
2253        ctx.set_preset(ThemePreset::Light);
2254        assert_eq!(hits.load(Ordering::SeqCst), 1);
2255
2256        ctx.remove_listener(id);
2257        ctx.set_preset(ThemePreset::Dark);
2258        assert_eq!(hits.load(Ordering::SeqCst), 1);
2259    }
2260
2261    #[test]
2262    fn test_theme_context_thread_safe() {
2263        use std::thread;
2264
2265        let ctx = Arc::new(ThemeContext::from_preset(ThemePreset::Dark));
2266        let handles: Vec<_> = (0..8)
2267            .map(|i| {
2268                let ctx = Arc::clone(&ctx);
2269                thread::spawn(move || {
2270                    if i % 2 == 0 {
2271                        ctx.set_preset(ThemePreset::Light);
2272                    } else {
2273                        ctx.set_preset(ThemePreset::Dark);
2274                    }
2275                    let _current = ctx.current();
2276                })
2277            })
2278            .collect();
2279
2280        for handle in handles {
2281            handle.join().expect("thread join");
2282        }
2283    }
2284
2285    #[test]
2286    fn test_theme_context_recovers_from_poisoned_current_lock() {
2287        let ctx = ThemeContext::from_preset(ThemePreset::Dark);
2288        let current = Arc::clone(&ctx.current);
2289
2290        let poison_result = std::thread::spawn(move || {
2291            let _guard = current.write().expect("write lock should be acquired");
2292            std::panic::resume_unwind(Box::new("poison current lock"));
2293        })
2294        .join();
2295        assert!(poison_result.is_err(), "poisoning thread should panic");
2296
2297        // Lock poisoning should not panic and should still allow theme updates.
2298        assert_eq!(ctx.current().name(), "Dark");
2299        ctx.set_preset(ThemePreset::Light);
2300        assert_eq!(ctx.current().name(), "Light");
2301    }
2302
2303    #[test]
2304    fn test_theme_context_recovers_from_poisoned_listeners_lock() {
2305        let ctx = ThemeContext::from_preset(ThemePreset::Dark);
2306        let listeners = Arc::clone(&ctx.listeners);
2307
2308        let poison_result = std::thread::spawn(move || {
2309            let _guard = listeners.write().expect("write lock should be acquired");
2310            std::panic::resume_unwind(Box::new("poison listeners lock"));
2311        })
2312        .join();
2313        assert!(poison_result.is_err(), "poisoning thread should panic");
2314
2315        let hits = Arc::new(AtomicUsize::new(0));
2316        let hits_ref = Arc::clone(&hits);
2317        let id = ctx.on_change(move |_theme| {
2318            hits_ref.fetch_add(1, Ordering::SeqCst);
2319        });
2320
2321        // Registering/listener notifications should continue working after poison.
2322        ctx.set_preset(ThemePreset::Light);
2323        assert_eq!(hits.load(Ordering::SeqCst), 1);
2324        ctx.remove_listener(id);
2325    }
2326
2327    #[test]
2328    fn test_theme_builder() {
2329        let theme = Theme::dark().with_name("Custom Dark").with_dark(true);
2330        assert_eq!(theme.name(), "Custom Dark");
2331        assert!(theme.is_dark());
2332    }
2333
2334    #[test]
2335    fn test_theme_colors_uniform() {
2336        let colors = ThemeColors::uniform("#ff0000");
2337        assert_eq!(colors.primary.0, "#ff0000");
2338        assert_eq!(colors.background.0, "#ff0000");
2339        assert_eq!(colors.text.0, "#ff0000");
2340    }
2341
2342    #[test]
2343    fn test_adaptive_color() {
2344        let light = ThemeColors::light();
2345        let dark = ThemeColors::dark();
2346
2347        let adaptive_text = adaptive(&light, &dark, |c| &c.text);
2348
2349        // Light theme text is dark, dark theme text is light
2350        assert_eq!(adaptive_text.light.0, light.text.0);
2351        assert_eq!(adaptive_text.dark.0, dark.text.0);
2352    }
2353
2354    #[test]
2355    fn test_theme_style() {
2356        let theme = Theme::dark();
2357        let style = theme.style();
2358        // Style should be empty/default
2359        assert!(style.value().is_empty());
2360    }
2361
2362    #[test]
2363    fn test_theme_get_slot() {
2364        let theme = Theme::dark();
2365        assert_eq!(theme.get(ColorSlot::Primary).0, theme.colors().primary.0);
2366        assert_eq!(
2367            theme.get(ColorSlot::TextMuted).0,
2368            theme.colors().text_muted.0
2369        );
2370        assert_eq!(theme.get(ColorSlot::Foreground).0, theme.colors().text.0);
2371        assert_eq!(theme.get(ColorSlot::Text).0, theme.colors().text.0);
2372    }
2373
2374    #[test]
2375    fn test_theme_json_roundtrip() {
2376        let theme = Theme::dark()
2377            .with_description("A dark theme")
2378            .with_author("charmed_rust");
2379        let json = theme.to_json().expect("serialize theme");
2380        let loaded = Theme::from_json(&json).expect("deserialize theme");
2381        assert_eq!(loaded.colors().primary.0, theme.colors().primary.0);
2382        assert_eq!(loaded.description(), Some("A dark theme"));
2383        assert_eq!(loaded.author(), Some("charmed_rust"));
2384        assert!(loaded.is_dark());
2385    }
2386
2387    #[test]
2388    fn test_theme_toml_roundtrip() {
2389        let theme = Theme::dark().with_description("TOML theme");
2390        let toml = theme.to_toml().expect("serialize theme to toml");
2391        let loaded = Theme::from_toml(&toml).expect("deserialize theme from toml");
2392        assert_eq!(loaded.colors().primary.0, theme.colors().primary.0);
2393        assert_eq!(loaded.description(), Some("TOML theme"));
2394        assert!(loaded.is_dark());
2395    }
2396
2397    #[test]
2398    fn test_theme_custom_colors_serde() {
2399        let mut theme = Theme::dark();
2400        theme
2401            .colors_mut()
2402            .custom_mut()
2403            .insert("brand".to_string(), Color::from("#123456"));
2404        let json = theme.to_json().expect("serialize theme");
2405        let loaded = Theme::from_json(&json).expect("deserialize theme");
2406        assert_eq!(
2407            loaded.colors().get_custom("brand").expect("custom color"),
2408            &Color::from("#123456")
2409        );
2410    }
2411
2412    #[test]
2413    fn test_color_slots_all_defined() {
2414        // Ensure all themes have all color slots defined (not empty)
2415        for theme in [
2416            Theme::dark(),
2417            Theme::light(),
2418            Theme::dracula(),
2419            Theme::nord(),
2420            Theme::catppuccin_mocha(),
2421            Theme::catppuccin_latte(),
2422            Theme::catppuccin_frappe(),
2423            Theme::catppuccin_macchiato(),
2424        ] {
2425            let c = theme.colors();
2426
2427            // All colors should have non-empty values
2428            assert!(!c.primary.0.is_empty(), "{}: primary empty", theme.name());
2429            assert!(
2430                !c.secondary.0.is_empty(),
2431                "{}: secondary empty",
2432                theme.name()
2433            );
2434            assert!(!c.accent.0.is_empty(), "{}: accent empty", theme.name());
2435            assert!(
2436                !c.background.0.is_empty(),
2437                "{}: background empty",
2438                theme.name()
2439            );
2440            assert!(!c.surface.0.is_empty(), "{}: surface empty", theme.name());
2441            assert!(!c.text.0.is_empty(), "{}: text empty", theme.name());
2442            assert!(!c.error.0.is_empty(), "{}: error empty", theme.name());
2443        }
2444    }
2445
2446    #[test]
2447    fn test_color_transform_lighten_darken() {
2448        let black = Color::from("#000000");
2449        let lighter = ColorTransform::Lighten(0.2).apply(black);
2450        assert_eq!(lighter.0, "#333333");
2451
2452        let white = Color::from("#ffffff");
2453        let darker = ColorTransform::Darken(0.2).apply(white);
2454        assert_eq!(darker.0, "#cccccc");
2455    }
2456
2457    #[test]
2458    fn test_color_transform_desaturate_and_alpha() {
2459        let red = Color::from("#ff0000");
2460        let gray = ColorTransform::Desaturate(1.0).apply(red);
2461        assert_eq!(gray.0, "#808080");
2462
2463        let white = Color::from("#ffffff");
2464        let alpha = ColorTransform::Alpha(0.5).apply(white);
2465        assert_eq!(alpha.0, "#808080");
2466    }
2467
2468    #[test]
2469    fn test_cached_themed_style_invalidation() {
2470        let ctx = Arc::new(ThemeContext::from_preset(ThemePreset::Dark));
2471        let themed = ThemedStyle::new(Arc::clone(&ctx))
2472            .background(ColorSlot::Background)
2473            .renderer(Arc::new(Renderer::DEFAULT));
2474        let cached = CachedThemedStyle::new(themed);
2475
2476        let first = cached.render("x");
2477        ctx.set_preset(ThemePreset::Light);
2478        let second = cached.render("x");
2479
2480        assert_ne!(first, second);
2481    }
2482}