1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
5#[serde(rename_all = "kebab-case")]
6pub enum Theme {
7 #[default]
8 Default,
9 Claude,
10 Twitter,
11 NeoBrutalism,
12 RetroArcade,
13 Aurora,
14 Business,
15}
16
17impl Theme {
18 pub const ALL: &[Self] = &[
19 Self::Default,
20 Self::Claude,
21 Self::Twitter,
22 Self::NeoBrutalism,
23 Self::RetroArcade,
24 Self::Aurora,
25 Self::Business,
26 ];
27
28 #[must_use]
29 pub const fn name(&self) -> &'static str {
30 match self {
31 Self::Default => "Default",
32 Self::Claude => "Claude",
33 Self::Twitter => "Twitter",
34 Self::NeoBrutalism => "Neo Brutalism",
35 Self::RetroArcade => "Retro Arcade",
36 Self::Aurora => "Aurora",
37 Self::Business => "Business",
38 }
39 }
40
41 #[must_use]
42 pub const fn css_class(&self) -> &'static str {
43 match self {
44 Self::Default => "",
45 Self::Claude => "theme-claude",
46 Self::Twitter => "theme-twitter",
47 Self::NeoBrutalism => "theme-neo-brutalism",
48 Self::RetroArcade => "theme-retro-arcade",
49 Self::Aurora => "theme-aurora",
50 Self::Business => "theme-business",
51 }
52 }
53
54 #[must_use]
55 #[allow(clippy::too_many_lines)]
56 pub const fn colors(&self) -> ThemeColors {
57 match self {
58 Self::Default => ThemeColors {
59 primary: AnsiColor(36),
60 secondary: AnsiColor(244),
61 background: AnsiColor(0),
62 foreground: AnsiColor(15),
63 accent: AnsiColor(33),
64 muted: AnsiColor(240),
65 border: AnsiColor(238),
66 pending: AnsiColor(226),
67 in_progress: AnsiColor(51),
68 done: AnsiColor(46),
69 blocked: AnsiColor(196),
70 error: AnsiColor(196),
71 warning: AnsiColor(214),
72 css_primary: "#06b6d4",
73 css_secondary: "#64748b",
74 css_background: "#0f172a",
75 css_foreground: "#f8fafc",
76 },
77 Self::Claude => ThemeColors {
78 primary: AnsiColor(208),
79 secondary: AnsiColor(180),
80 background: AnsiColor(230),
81 foreground: AnsiColor(236),
82 accent: AnsiColor(215),
83 muted: AnsiColor(249),
84 border: AnsiColor(223),
85 pending: AnsiColor(226),
86 in_progress: AnsiColor(51),
87 done: AnsiColor(46),
88 blocked: AnsiColor(196),
89 error: AnsiColor(196),
90 warning: AnsiColor(214),
91 css_primary: "#d97706",
92 css_secondary: "#92400e",
93 css_background: "#fffbeb",
94 css_foreground: "#1c1917",
95 },
96 Self::Twitter => ThemeColors {
97 primary: AnsiColor(33),
98 secondary: AnsiColor(244),
99 background: AnsiColor(16),
100 foreground: AnsiColor(15),
101 accent: AnsiColor(39),
102 muted: AnsiColor(240),
103 border: AnsiColor(236),
104 pending: AnsiColor(226),
105 in_progress: AnsiColor(51),
106 done: AnsiColor(46),
107 blocked: AnsiColor(196),
108 error: AnsiColor(196),
109 warning: AnsiColor(214),
110 css_primary: "#1d9bf0",
111 css_secondary: "#71767b",
112 css_background: "#000000",
113 css_foreground: "#e7e9ea",
114 },
115 Self::NeoBrutalism => ThemeColors {
116 primary: AnsiColor(226),
117 secondary: AnsiColor(201),
118 background: AnsiColor(15),
119 foreground: AnsiColor(16),
120 accent: AnsiColor(196),
121 muted: AnsiColor(250),
122 border: AnsiColor(16),
123 pending: AnsiColor(226),
124 in_progress: AnsiColor(51),
125 done: AnsiColor(46),
126 blocked: AnsiColor(196),
127 error: AnsiColor(196),
128 warning: AnsiColor(214),
129 css_primary: "#facc15",
130 css_secondary: "#ec4899",
131 css_background: "#ffffff",
132 css_foreground: "#000000",
133 },
134 Self::RetroArcade => ThemeColors {
135 primary: AnsiColor(46),
136 secondary: AnsiColor(51),
137 background: AnsiColor(16),
138 foreground: AnsiColor(46),
139 accent: AnsiColor(201),
140 muted: AnsiColor(22),
141 border: AnsiColor(28),
142 pending: AnsiColor(226),
143 in_progress: AnsiColor(51),
144 done: AnsiColor(46),
145 blocked: AnsiColor(196),
146 error: AnsiColor(196),
147 warning: AnsiColor(214),
148 css_primary: "#22c55e",
149 css_secondary: "#06b6d4",
150 css_background: "#000000",
151 css_foreground: "#22c55e",
152 },
153 Self::Aurora => ThemeColors {
154 primary: AnsiColor(141),
155 secondary: AnsiColor(80),
156 background: AnsiColor(17),
157 foreground: AnsiColor(189),
158 accent: AnsiColor(213),
159 muted: AnsiColor(60),
160 border: AnsiColor(61),
161 pending: AnsiColor(226),
162 in_progress: AnsiColor(51),
163 done: AnsiColor(46),
164 blocked: AnsiColor(196),
165 error: AnsiColor(196),
166 warning: AnsiColor(214),
167 css_primary: "#8b5cf6",
168 css_secondary: "#14b8a6",
169 css_background: "#0f0a2a",
170 css_foreground: "#c4b5fd",
171 },
172 Self::Business => ThemeColors {
173 primary: AnsiColor(24),
174 secondary: AnsiColor(244),
175 background: AnsiColor(255),
176 foreground: AnsiColor(235),
177 accent: AnsiColor(32),
178 muted: AnsiColor(250),
179 border: AnsiColor(252),
180 pending: AnsiColor(226),
181 in_progress: AnsiColor(51),
182 done: AnsiColor(46),
183 blocked: AnsiColor(196),
184 error: AnsiColor(196),
185 warning: AnsiColor(214),
186 css_primary: "#1e3a5f",
187 css_secondary: "#64748b",
188 css_background: "#ffffff",
189 css_foreground: "#1e293b",
190 },
191 }
192 }
193
194 #[must_use]
196 pub fn next(&self) -> Self {
197 let all = Self::ALL;
198 let idx = all.iter().position(|t| t == self).unwrap_or(0);
199 all[(idx + 1) % all.len()]
200 }
201}
202
203#[derive(Debug, Clone, Copy)]
205pub struct AnsiColor(pub u8);
206
207impl AnsiColor {
208 #[must_use]
210 pub fn fg(&self) -> String {
211 format!("\x1b[38;5;{}m", self.0)
212 }
213
214 #[must_use]
216 pub fn bg(&self) -> String {
217 format!("\x1b[48;5;{}m", self.0)
218 }
219}
220
221#[derive(Debug, Clone)]
223pub struct ThemeColors {
224 pub primary: AnsiColor,
226 pub secondary: AnsiColor,
227 pub background: AnsiColor,
228 pub foreground: AnsiColor,
229 pub accent: AnsiColor,
230 pub muted: AnsiColor,
231 pub border: AnsiColor,
232
233 pub pending: AnsiColor,
235 pub in_progress: AnsiColor,
236 pub done: AnsiColor,
237 pub blocked: AnsiColor,
238 pub error: AnsiColor,
239 pub warning: AnsiColor,
240
241 pub css_primary: &'static str,
243 pub css_secondary: &'static str,
244 pub css_background: &'static str,
245 pub css_foreground: &'static str,
246}
247
248impl ThemeColors {
249 #[must_use]
251 pub fn to_css_variables(&self) -> String {
252 format!(
253 ":root {{\n --primary: {};\n --secondary: {};\n --background: {};\n --foreground: {};\n}}",
254 self.css_primary, self.css_secondary, self.css_background, self.css_foreground
255 )
256 }
257}
258
259impl std::fmt::Display for Theme {
260 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
261 write!(f, "{}", self.name())
262 }
263}
264
265impl std::str::FromStr for Theme {
266 type Err = String;
267
268 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
269 match s.to_lowercase().as_str() {
270 "default" => Ok(Self::Default),
271 "claude" => Ok(Self::Claude),
272 "twitter" => Ok(Self::Twitter),
273 "neo-brutalism" | "neobrutalism" | "neo_brutalism" => Ok(Self::NeoBrutalism),
274 "retro-arcade" | "retroarcade" | "retro_arcade" => Ok(Self::RetroArcade),
275 "aurora" => Ok(Self::Aurora),
276 "business" => Ok(Self::Business),
277 _ => Err(format!("unknown theme: {s}")),
278 }
279 }
280}