Skip to main content

utils/
themes.rs

1use serde::Deserialize;
2use std::collections::HashMap;
3
4const VAR_MAP: &[(&str, &str)] = &[
5    ("bg", "--color-black"),
6    ("text", "--color-white"),
7    ("text-muted", "--color-slate-400"),
8    ("surface", "--color-slate-500"),
9    ("progress", "--color-green-500"),
10    ("accent-soft", "--color-indigo-400"),
11    ("accent", "--color-indigo-500"),
12    ("accent-alt", "--color-indigo-600"),
13    ("accent-deep", "--color-indigo-900"),
14    ("highlight", "--color-purple-600"),
15    ("highlight-dark", "--color-purple-700"),
16    ("danger", "--color-red-400"),
17    ("raised", "--color-neutral-900"),
18];
19
20#[derive(Debug, Clone, PartialEq)]
21pub enum ThemeKind {
22    Dark,
23    Light,
24}
25
26#[derive(Debug, Clone)]
27pub struct Theme {
28    pub id: String,
29    pub name: String,
30    pub kind: ThemeKind,
31    pub vars: HashMap<String, String>,
32}
33
34impl Theme {
35    pub fn var(&self, key: &str) -> Option<&str> {
36        self.vars.get(key).map(String::as_str)
37    }
38
39    // Maps values back to the css custom properties Kopuz uses.
40    pub fn to_css(&self) -> String {
41        let mut out = format!(".theme-{} {{\n", self.id);
42        for (purpose, css_var) in VAR_MAP {
43            if let Some(val) = self.var(purpose) {
44                out.push_str(&format!("    {}: {};\n", css_var, val));
45            }
46        }
47        out.push('}');
48        out
49    }
50}
51
52#[derive(Deserialize)]
53struct RawTheme {
54    name: String,
55    #[serde(flatten)]
56    vars: HashMap<String, String>,
57}
58
59#[derive(Deserialize)]
60struct ThemeFile {
61    dark: HashMap<String, RawTheme>,
62    light: HashMap<String, RawTheme>,
63}
64
65pub fn load_themes() -> Vec<Theme> {
66    let path = std::env::var("KOPUZ_THEMES_PATH")
67        .map(std::path::PathBuf::from)
68        .unwrap_or_else(|_| {
69            std::env::current_exe()
70                .ok()
71                .and_then(|p| p.parent().map(|d| d.join("assets/themes.json")))
72                .unwrap_or_else(|| std::path::PathBuf::from("assets/themes.json"))
73        });
74    let json = match std::fs::read_to_string(&path) {
75        Ok(j) => j,
76        Err(e) => {
77            tracing::warn!("failed to read themes.json from {}: {e}", path.display());
78            return Vec::new();
79        }
80    };
81    let file: ThemeFile = match serde_json::from_str(&json) {
82        Ok(f) => f,
83        Err(e) => {
84            tracing::warn!("themes.json is malformed: {e}");
85            return Vec::new();
86        }
87    };
88
89    let mut themes = Vec::new();
90
91    for (id, raw) in file.dark {
92        themes.push(Theme {
93            id,
94            name: raw.name,
95            kind: ThemeKind::Dark,
96            vars: raw.vars,
97        });
98    }
99    for (id, raw) in file.light {
100        themes.push(Theme {
101            id,
102            name: raw.name,
103            kind: ThemeKind::Light,
104            vars: raw.vars,
105        });
106    }
107
108    themes
109}
110
111pub fn theme_map() -> HashMap<String, Theme> {
112    load_themes()
113        .into_iter()
114        .map(|t| (t.id.clone(), t))
115        .collect()
116}
117
118pub fn all_themes_css() -> String {
119    load_themes()
120        .iter()
121        .map(|t| t.to_css())
122        .collect::<Vec<_>>()
123        .join("\n\n")
124}
125
126/// Generate a CSS block for a single custom theme given its id and var map.
127pub fn custom_theme_to_css(id: &str, vars: &std::collections::HashMap<String, String>) -> String {
128    let theme = Theme {
129        id: id.to_string(),
130        name: String::new(),
131        kind: ThemeKind::Dark,
132        vars: vars.clone(),
133    };
134    theme.to_css()
135}