1use crate::style::{Color, Style};
7use std::{error::Error, fmt, str::FromStr};
8
9static BUILTIN_THEMES: [BuiltinTheme; 4] = BuiltinTheme::ALL;
10static THEME_ROLES: [ThemeRole; 12] = ThemeRole::ALL;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum BuiltinTheme {
15 Dark,
16 Light,
17 Catppuccin,
18 TokyoNight,
19}
20
21impl BuiltinTheme {
22 pub const ALL: [Self; 4] = [Self::Dark, Self::Light, Self::Catppuccin, Self::TokyoNight];
23
24 pub fn name(self) -> &'static str {
26 match self {
27 Self::Dark => "dark",
28 Self::Light => "light",
29 Self::Catppuccin => "catppuccin",
30 Self::TokyoNight => "tokyo-night",
31 }
32 }
33
34 pub fn label(self) -> &'static str {
36 match self {
37 Self::Dark => "Dark",
38 Self::Light => "Light",
39 Self::Catppuccin => "Catppuccin",
40 Self::TokyoNight => "Tokyo Night",
41 }
42 }
43
44 pub fn theme(self) -> Theme {
46 match self {
47 Self::Dark => Theme::dark(),
48 Self::Light => Theme::light(),
49 Self::Catppuccin => Theme::catppuccin(),
50 Self::TokyoNight => Theme::tokyo_night(),
51 }
52 }
53}
54
55impl FromStr for BuiltinTheme {
56 type Err = ParseBuiltinThemeError;
57
58 fn from_str(value: &str) -> Result<Self, Self::Err> {
59 match normalized_token_name(value).as_str() {
60 "dark" => Ok(Self::Dark),
61 "light" => Ok(Self::Light),
62 "catppuccin" | "catppuccin-mocha" => Ok(Self::Catppuccin),
63 "tokyo-night" | "tokyonight" => Ok(Self::TokyoNight),
64 _ => Err(ParseBuiltinThemeError),
65 }
66 }
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub struct ParseBuiltinThemeError;
72
73impl fmt::Display for ParseBuiltinThemeError {
74 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75 f.write_str("unknown built-in theme")
76 }
77}
78
79impl Error for ParseBuiltinThemeError {}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum ThemeRole {
87 Primary,
88 Secondary,
89 Background,
90 Foreground,
91 Muted,
92 Border,
93 Success,
94 Warning,
95 Error,
96 Info,
97 Surface,
98 Highlight,
99}
100
101impl ThemeRole {
102 pub const ALL: [Self; 12] = [
103 Self::Primary,
104 Self::Secondary,
105 Self::Background,
106 Self::Foreground,
107 Self::Muted,
108 Self::Border,
109 Self::Success,
110 Self::Warning,
111 Self::Error,
112 Self::Info,
113 Self::Surface,
114 Self::Highlight,
115 ];
116
117 pub fn name(self) -> &'static str {
119 match self {
120 Self::Primary => "primary",
121 Self::Secondary => "secondary",
122 Self::Background => "background",
123 Self::Foreground => "foreground",
124 Self::Muted => "muted",
125 Self::Border => "border",
126 Self::Success => "success",
127 Self::Warning => "warning",
128 Self::Error => "error",
129 Self::Info => "info",
130 Self::Surface => "surface",
131 Self::Highlight => "highlight",
132 }
133 }
134
135 pub fn label(self) -> &'static str {
137 match self {
138 Self::Primary => "Primary",
139 Self::Secondary => "Secondary",
140 Self::Background => "Background",
141 Self::Foreground => "Foreground",
142 Self::Muted => "Muted",
143 Self::Border => "Border",
144 Self::Success => "Success",
145 Self::Warning => "Warning",
146 Self::Error => "Error",
147 Self::Info => "Info",
148 Self::Surface => "Surface",
149 Self::Highlight => "Highlight",
150 }
151 }
152}
153
154impl FromStr for ThemeRole {
155 type Err = ParseThemeRoleError;
156
157 fn from_str(value: &str) -> Result<Self, Self::Err> {
158 match normalized_token_name(value).as_str() {
159 "primary" => Ok(Self::Primary),
160 "secondary" => Ok(Self::Secondary),
161 "background" | "bg" => Ok(Self::Background),
162 "foreground" | "fg" | "text" => Ok(Self::Foreground),
163 "muted" | "dim" => Ok(Self::Muted),
164 "border" => Ok(Self::Border),
165 "success" | "ok" => Ok(Self::Success),
166 "warning" | "warn" => Ok(Self::Warning),
167 "error" | "danger" => Ok(Self::Error),
168 "info" => Ok(Self::Info),
169 "surface" => Ok(Self::Surface),
170 "highlight" | "selection" => Ok(Self::Highlight),
171 _ => Err(ParseThemeRoleError),
172 }
173 }
174}
175
176#[derive(Debug, Clone, Copy, PartialEq, Eq)]
178pub struct ParseThemeRoleError;
179
180impl fmt::Display for ParseThemeRoleError {
181 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182 f.write_str("unknown theme role")
183 }
184}
185
186impl Error for ParseThemeRoleError {}
187
188#[derive(Debug, Clone)]
193pub struct Theme {
194 pub primary: Color,
196 pub secondary: Color,
198 pub bg: Color,
200 pub fg: Color,
202 pub muted: Color,
204 pub border: Color,
206 pub success: Color,
208 pub warning: Color,
210 pub error: Color,
212 pub info: Color,
214 pub surface: Color,
216 pub highlight: Color,
218}
219
220impl Theme {
221 pub fn builtins() -> &'static [BuiltinTheme] {
223 &BUILTIN_THEMES
224 }
225
226 pub fn builtin(theme: BuiltinTheme) -> Self {
228 theme.theme()
229 }
230
231 pub fn from_builtin_name(name: &str) -> Option<Self> {
233 name.parse::<BuiltinTheme>().ok().map(Self::builtin)
234 }
235
236 pub fn roles() -> &'static [ThemeRole] {
238 &THEME_ROLES
239 }
240
241 pub fn color(&self, role: ThemeRole) -> Color {
243 match role {
244 ThemeRole::Primary => self.primary,
245 ThemeRole::Secondary => self.secondary,
246 ThemeRole::Background => self.bg,
247 ThemeRole::Foreground => self.fg,
248 ThemeRole::Muted => self.muted,
249 ThemeRole::Border => self.border,
250 ThemeRole::Success => self.success,
251 ThemeRole::Warning => self.warning,
252 ThemeRole::Error => self.error,
253 ThemeRole::Info => self.info,
254 ThemeRole::Surface => self.surface,
255 ThemeRole::Highlight => self.highlight,
256 }
257 }
258
259 pub fn foreground_style(&self, role: ThemeRole) -> Style {
261 Style::new().fg(self.color(role))
262 }
263
264 pub fn background_style(&self, role: ThemeRole) -> Style {
266 Style::new().bg(self.color(role))
267 }
268
269 pub fn pair_style(&self, fg: ThemeRole, bg: ThemeRole) -> Style {
271 Style::new().fg(self.color(fg)).bg(self.color(bg))
272 }
273
274 pub fn selection_style(&self) -> Style {
276 self.pair_style(ThemeRole::Foreground, ThemeRole::Highlight)
277 .bold()
278 }
279
280 pub fn surface_style(&self) -> Style {
282 self.pair_style(ThemeRole::Foreground, ThemeRole::Surface)
283 }
284
285 pub fn dark() -> Self {
287 Self {
288 primary: Color::Cyan,
289 secondary: Color::Blue,
290 bg: Color::Black,
291 fg: Color::White,
292 muted: Color::BrightBlack,
293 border: Color::BrightBlack,
294 success: Color::Green,
295 warning: Color::Yellow,
296 error: Color::Red,
297 info: Color::Blue,
298 surface: Color::BrightBlack,
299 highlight: Color::BrightBlue,
300 }
301 }
302
303 pub fn light() -> Self {
305 Self {
306 primary: Color::Blue,
307 secondary: Color::Cyan,
308 bg: Color::White,
309 fg: Color::Black,
310 muted: Color::BrightBlack,
311 border: Color::BrightBlack,
312 success: Color::Green,
313 warning: Color::Yellow,
314 error: Color::Red,
315 info: Color::Blue,
316 surface: Color::BrightWhite,
317 highlight: Color::BrightCyan,
318 }
319 }
320
321 pub fn catppuccin() -> Self {
323 Self {
324 primary: Color::Rgb(137, 180, 250), secondary: Color::Rgb(180, 190, 254), bg: Color::Rgb(30, 30, 46), fg: Color::Rgb(205, 214, 244), muted: Color::Rgb(127, 132, 156), border: Color::Rgb(88, 91, 112), success: Color::Rgb(166, 227, 161), warning: Color::Rgb(249, 226, 175), error: Color::Rgb(243, 139, 168), info: Color::Rgb(116, 199, 236), surface: Color::Rgb(49, 50, 68), highlight: Color::Rgb(69, 71, 90), }
337 }
338
339 pub fn tokyo_night() -> Self {
341 Self {
342 primary: Color::Rgb(122, 162, 247), secondary: Color::Rgb(187, 154, 247), bg: Color::Rgb(26, 27, 38), fg: Color::Rgb(192, 202, 245), muted: Color::Rgb(86, 95, 137), border: Color::Rgb(61, 89, 161), success: Color::Rgb(158, 206, 106), warning: Color::Rgb(224, 175, 104), error: Color::Rgb(247, 118, 142), info: Color::Rgb(125, 207, 255), surface: Color::Rgb(36, 40, 59), highlight: Color::Rgb(41, 46, 66), }
355 }
356}
357
358fn normalized_token_name(value: &str) -> String {
359 value
360 .trim()
361 .chars()
362 .map(|ch| match ch {
363 '_' | ' ' => '-',
364 ch => ch.to_ascii_lowercase(),
365 })
366 .collect()
367}
368
369impl Default for Theme {
370 fn default() -> Self {
371 Self::dark()
372 }
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378
379 #[test]
380 fn default_is_dark() {
381 let theme = Theme::default();
382 assert_eq!(theme.bg, Color::Black);
383 assert_eq!(theme.fg, Color::White);
384 }
385
386 #[test]
387 fn light_theme_has_white_bg() {
388 let theme = Theme::light();
389 assert_eq!(theme.bg, Color::White);
390 assert_eq!(theme.fg, Color::Black);
391 }
392
393 #[test]
394 fn catppuccin_uses_rgb() {
395 let theme = Theme::catppuccin();
396 assert!(matches!(theme.primary, Color::Rgb(_, _, _)));
397 assert!(matches!(theme.bg, Color::Rgb(_, _, _)));
398 }
399
400 #[test]
401 fn tokyo_night_uses_rgb() {
402 let theme = Theme::tokyo_night();
403 assert!(matches!(theme.primary, Color::Rgb(_, _, _)));
404 assert!(matches!(theme.bg, Color::Rgb(_, _, _)));
405 }
406
407 #[test]
408 fn built_in_themes_parse_friendly_names() {
409 assert_eq!("dark".parse::<BuiltinTheme>(), Ok(BuiltinTheme::Dark));
410 assert_eq!(
411 "Tokyo Night".parse::<BuiltinTheme>(),
412 Ok(BuiltinTheme::TokyoNight)
413 );
414 assert_eq!(
415 "catppuccin_mocha".parse::<BuiltinTheme>(),
416 Ok(BuiltinTheme::Catppuccin)
417 );
418 assert!("missing".parse::<BuiltinTheme>().is_err());
419 assert_eq!(
420 Theme::from_builtin_name("tokyo-night").unwrap().primary,
421 Theme::tokyo_night().primary
422 );
423 }
424
425 #[test]
426 fn theme_roles_map_to_palette_colors() {
427 let theme = Theme::dark();
428
429 assert_eq!(Theme::roles().len(), ThemeRole::ALL.len());
430 assert_eq!(theme.color(ThemeRole::Primary), theme.primary);
431 assert_eq!(theme.color(ThemeRole::Background), theme.bg);
432 assert_eq!(theme.color(ThemeRole::Highlight), theme.highlight);
433 assert_eq!("fg".parse::<ThemeRole>(), Ok(ThemeRole::Foreground));
434 assert_eq!("danger".parse::<ThemeRole>(), Ok(ThemeRole::Error));
435 assert!("unknown".parse::<ThemeRole>().is_err());
436 }
437
438 #[test]
439 fn theme_style_helpers_use_roles() {
440 let theme = Theme::tokyo_night();
441 let primary = theme.foreground_style(ThemeRole::Primary);
442 let selected = theme.selection_style();
443 let surface = theme.surface_style();
444
445 assert_eq!(primary.foreground(), Some(theme.primary));
446 assert_eq!(selected.foreground(), Some(theme.fg));
447 assert_eq!(selected.background(), Some(theme.highlight));
448 assert!(selected.is_bold());
449 assert_eq!(surface.foreground(), Some(theme.fg));
450 assert_eq!(surface.background(), Some(theme.surface));
451 }
452}