ratatui_toolkit/services/theme/loader/
load_theme_str.rs

1//! Load theme from JSON string.
2
3use std::collections::HashMap;
4
5use ratatui::style::Color;
6
7use crate::services::theme::app_theme::AppTheme;
8use crate::services::theme::diff_colors::DiffColors;
9use crate::services::theme::loader::resolve_defs::resolve_color_value;
10use crate::services::theme::loader::theme_json::{ColorValue, ThemeJson};
11use crate::services::theme::markdown_colors::MarkdownColors;
12use crate::services::theme::syntax_colors::SyntaxColors;
13use crate::services::theme::ThemeVariant;
14
15/// Loads an [`AppTheme`] from a JSON string in opencode format.
16///
17/// Parses the JSON, resolves all color references, and constructs
18/// a complete `AppTheme` with all color categories populated.
19///
20/// # Arguments
21///
22/// * `json` - The JSON string to parse
23/// * `variant` - Which theme variant (dark/light) to use
24///
25/// # Returns
26///
27/// `Ok(AppTheme)` if parsing and resolution succeeds,
28/// `Err` with a description if parsing fails.
29///
30/// # Errors
31///
32/// Returns an error if:
33/// - The JSON is malformed
34/// - Required color keys are missing
35/// - Color values cannot be resolved
36///
37/// # Example
38///
39/// ```rust
40/// use ratatui_toolkit::services::theme::{loader, ThemeVariant};
41///
42/// let json = r#"{
43///   "defs": { "bg": "#282828", "fg": "#ebdbb2" },
44///   "theme": {
45///     "background": "bg",
46///     "text": "fg"
47///   }
48/// }"#;
49///
50/// let theme = loader::load_theme_str(json, ThemeVariant::Dark);
51/// assert!(theme.is_ok());
52/// ```
53pub fn load_theme_str(json: &str, variant: ThemeVariant) -> Result<AppTheme, String> {
54    let theme_json: ThemeJson =
55        serde_json::from_str(json).map_err(|e| format!("Failed to parse JSON: {}", e))?;
56
57    build_theme_from_json(&theme_json, variant)
58}
59
60/// Builds an AppTheme from parsed ThemeJson.
61pub(crate) fn build_theme_from_json(
62    theme_json: &ThemeJson,
63    variant: ThemeVariant,
64) -> Result<AppTheme, String> {
65    let defs = &theme_json.defs;
66    let theme = &theme_json.theme;
67
68    // Helper to resolve a color with a default
69    let resolve = |key: &str, default: Color| -> Color {
70        theme
71            .get(key)
72            .and_then(|v| resolve_color_value(v, defs, variant))
73            .unwrap_or(default)
74    };
75
76    // Get default theme for fallback values
77    let default = AppTheme::default();
78
79    // Build UI colors
80    let primary = resolve("primary", default.primary);
81    let secondary = resolve("secondary", default.secondary);
82    let accent = resolve("accent", default.accent);
83    let error = resolve("error", default.error);
84    let warning = resolve("warning", default.warning);
85    let success = resolve("success", default.success);
86    let info = resolve("info", default.info);
87
88    // Build text colors
89    let text = resolve("text", default.text);
90    let text_muted = resolve("textMuted", default.text_muted);
91    // Selected text falls back to text if not specified
92    let selected_text = theme
93        .get("selectedText")
94        .and_then(|v| resolve_color_value(v, defs, variant))
95        .unwrap_or(text);
96
97    // Build background colors
98    let background = resolve("background", default.background);
99    let background_panel = resolve("backgroundPanel", default.background_panel);
100    let background_element = resolve("backgroundElement", default.background_element);
101    // Menu background falls back to panel if not specified
102    let background_menu = theme
103        .get("backgroundMenu")
104        .and_then(|v| resolve_color_value(v, defs, variant))
105        .unwrap_or(background_panel);
106
107    // Build border colors
108    let border = resolve("border", default.border);
109    let border_active = resolve("borderActive", default.border_active);
110    let border_subtle = resolve("borderSubtle", default.border_subtle);
111
112    // Build diff colors
113    let diff = build_diff_colors(theme, defs, variant);
114
115    // Build markdown colors
116    let markdown = build_markdown_colors(theme, defs, variant);
117
118    // Build syntax colors
119    let syntax = build_syntax_colors(theme, defs, variant);
120
121    Ok(AppTheme {
122        primary,
123        secondary,
124        accent,
125        error,
126        warning,
127        success,
128        info,
129        text,
130        text_muted,
131        selected_text,
132        background,
133        background_panel,
134        background_element,
135        background_menu,
136        border,
137        border_active,
138        border_subtle,
139        diff,
140        markdown,
141        syntax,
142    })
143}
144
145/// Builds DiffColors from theme JSON.
146fn build_diff_colors(
147    theme: &HashMap<String, ColorValue>,
148    defs: &HashMap<String, String>,
149    variant: ThemeVariant,
150) -> DiffColors {
151    let default = DiffColors::default();
152
153    let resolve = |key: &str, default: Color| -> Color {
154        theme
155            .get(key)
156            .and_then(|v| resolve_color_value(v, defs, variant))
157            .unwrap_or(default)
158    };
159
160    DiffColors {
161        added: resolve("diffAdded", default.added),
162        removed: resolve("diffRemoved", default.removed),
163        context: resolve("diffContext", default.context),
164        hunk_header: resolve("diffHunkHeader", default.hunk_header),
165        highlight_added: resolve("diffHighlightAdded", default.highlight_added),
166        highlight_removed: resolve("diffHighlightRemoved", default.highlight_removed),
167        added_bg: resolve("diffAddedBg", default.added_bg),
168        removed_bg: resolve("diffRemovedBg", default.removed_bg),
169        context_bg: resolve("diffContextBg", default.context_bg),
170        line_number: resolve("diffLineNumber", default.line_number),
171        added_line_number_bg: resolve("diffAddedLineNumberBg", default.added_line_number_bg),
172        removed_line_number_bg: resolve("diffRemovedLineNumberBg", default.removed_line_number_bg),
173    }
174}
175
176/// Builds MarkdownColors from theme JSON.
177fn build_markdown_colors(
178    theme: &HashMap<String, ColorValue>,
179    defs: &HashMap<String, String>,
180    variant: ThemeVariant,
181) -> MarkdownColors {
182    let default = MarkdownColors::default();
183
184    let resolve = |key: &str, default: Color| -> Color {
185        theme
186            .get(key)
187            .and_then(|v| resolve_color_value(v, defs, variant))
188            .unwrap_or(default)
189    };
190
191    MarkdownColors {
192        text: resolve("markdownText", default.text),
193        heading: resolve("markdownHeading", default.heading),
194        link: resolve("markdownLink", default.link),
195        link_text: resolve("markdownLinkText", default.link_text),
196        code: resolve("markdownCode", default.code),
197        block_quote: resolve("markdownBlockQuote", default.block_quote),
198        emph: resolve("markdownEmph", default.emph),
199        strong: resolve("markdownStrong", default.strong),
200        horizontal_rule: resolve("markdownHorizontalRule", default.horizontal_rule),
201        list_item: resolve("markdownListItem", default.list_item),
202        list_enumeration: resolve("markdownListEnumeration", default.list_enumeration),
203        image: resolve("markdownImage", default.image),
204        image_text: resolve("markdownImageText", default.image_text),
205        code_block: resolve("markdownCodeBlock", default.code_block),
206    }
207}
208
209/// Builds SyntaxColors from theme JSON.
210fn build_syntax_colors(
211    theme: &HashMap<String, ColorValue>,
212    defs: &HashMap<String, String>,
213    variant: ThemeVariant,
214) -> SyntaxColors {
215    let default = SyntaxColors::default();
216
217    let resolve = |key: &str, default: Color| -> Color {
218        theme
219            .get(key)
220            .and_then(|v| resolve_color_value(v, defs, variant))
221            .unwrap_or(default)
222    };
223
224    SyntaxColors {
225        comment: resolve("syntaxComment", default.comment),
226        keyword: resolve("syntaxKeyword", default.keyword),
227        function: resolve("syntaxFunction", default.function),
228        variable: resolve("syntaxVariable", default.variable),
229        string: resolve("syntaxString", default.string),
230        number: resolve("syntaxNumber", default.number),
231        type_: resolve("syntaxType", default.type_),
232        operator: resolve("syntaxOperator", default.operator),
233        punctuation: resolve("syntaxPunctuation", default.punctuation),
234    }
235}