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 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
126pub 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}