1use ratatui::style::{Color, Modifier, Style};
13
14#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
15pub enum ThemeKind {
16 Default,
17 Dark,
18 Mono,
19 HighContrast,
20}
21
22#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
23pub enum ColorLevel {
24 None,
25 Ansi16,
26 Ansi256,
27 TrueColor,
28}
29
30#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
31pub struct ThemeKey {
32 pub kind: ThemeKind,
33 pub colors: ColorLevel,
34}
35
36#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
38pub enum DecisionTone {
39 Allow,
40 Deny,
41 Neutral,
42}
43
44#[derive(Clone, Copy, Debug)]
45struct Swatch {
46 rgb: (u8, u8, u8),
47 ansi256: u8,
48 ansi16: Color,
49}
50
51#[derive(Debug)]
52struct Palette {
53 cream: Swatch,
54 terminal: Swatch,
55 parchment: Swatch,
56 glaze: Swatch,
57 toasted: Swatch,
58 crust: Swatch,
59 border: Swatch,
60 tan: Swatch,
61 espresso: Swatch,
62 cocoa: Swatch,
63 latte: Swatch,
64 ash: Swatch,
65 quote: Swatch,
66 caramel: Swatch,
67 sage: Swatch,
68 basil: Swatch,
69 cinnamon: Swatch,
70 cranberry: Swatch,
71 honey: Swatch,
72 maple: Swatch,
73 slate: Swatch,
74 plum: Swatch,
75 terracotta: Swatch,
76 allow_tint: Swatch,
77 deny_tint: Swatch,
78 neutral_tint: Swatch,
79 ink: Swatch,
81 pan: Swatch,
84}
85
86impl Palette {
87 fn swatches(&self) -> [Swatch; 28] {
88 [
89 self.cream,
90 self.terminal,
91 self.parchment,
92 self.glaze,
93 self.toasted,
94 self.crust,
95 self.border,
96 self.tan,
97 self.espresso,
98 self.cocoa,
99 self.latte,
100 self.ash,
101 self.quote,
102 self.caramel,
103 self.sage,
104 self.basil,
105 self.cinnamon,
106 self.cranberry,
107 self.honey,
108 self.maple,
109 self.slate,
110 self.plum,
111 self.terracotta,
112 self.allow_tint,
113 self.deny_tint,
114 self.neutral_tint,
115 self.ink,
116 self.pan,
117 ]
118 }
119
120 fn swatch_for_rgb(&self, rgb: (u8, u8, u8)) -> Option<Swatch> {
121 self.swatches().into_iter().find(|swatch| swatch.rgb == rgb)
122 }
123}
124
125const fn swatch(rgb: (u8, u8, u8), ansi256: u8, ansi16: Color) -> Swatch {
126 Swatch {
127 rgb,
128 ansi256,
129 ansi16,
130 }
131}
132
133const LIGHT: Palette = Palette {
136 cream: swatch((0xFF, 0xFF, 0xFF), 231, Color::White),
137 terminal: swatch((0xF0, 0xEE, 0xEA), 255, Color::White),
138 parchment: swatch((0xF2, 0xE5, 0xCC), 230, Color::Gray),
139 glaze: swatch((0xEB, 0xD8, 0xAE), 223, Color::Gray),
140 toasted: swatch((0xE6, 0xCE, 0x9E), 222, Color::Gray),
141 crust: swatch((0xC9, 0xAE, 0x85), 180, Color::LightCyan),
142 border: swatch((0x6B, 0x4F, 0x2C), 240, Color::DarkGray),
143 tan: swatch((0x71, 0x54, 0x30), 240, Color::DarkGray),
144 espresso: swatch((0x46, 0x30, 0x1F), 236, Color::Black),
145 cocoa: swatch((0x6E, 0x4E, 0x38), 95, Color::Cyan),
146 latte: swatch((0x7A, 0x59, 0x41), 95, Color::DarkGray),
147 ash: swatch((0x62, 0x5E, 0x66), 241, Color::DarkGray),
148 quote: swatch((0x6F, 0x62, 0x50), 243, Color::DarkGray),
149 caramel: swatch((0x9C, 0x5A, 0x10), 130, Color::Yellow),
150 sage: swatch((0x4E, 0x7A, 0x34), 65, Color::Green),
151 basil: swatch((0x2F, 0x6B, 0x38), 29, Color::Green),
152 cinnamon: swatch((0x9C, 0x4A, 0x12), 131, Color::Yellow),
153 cranberry: swatch((0xAE, 0x33, 0x27), 88, Color::Red),
154 honey: swatch((0x7A, 0x52, 0x06), 94, Color::Yellow),
155 maple: swatch((0x8A, 0x4A, 0x0B), 58, Color::Black),
156 slate: swatch((0x3D, 0x6A, 0x8C), 24, Color::Blue),
157 plum: swatch((0x8A, 0x55, 0x70), 96, Color::Magenta),
158 terracotta: swatch((0xA8, 0x47, 0x1C), 124, Color::Red),
159 allow_tint: swatch((0xDE, 0xE7, 0xC6), 194, Color::LightGreen),
160 deny_tint: swatch((0xF3, 0xD5, 0xC9), 224, Color::LightRed),
161 neutral_tint: swatch((0xE6, 0xDC, 0xC6), 187, Color::Gray),
162 ink: swatch((0x62, 0x52, 0x40), 239, Color::DarkGray),
163 pan: swatch((0xE7, 0xE4, 0xDF), 254, Color::Gray),
164};
165
166const DARK: Palette = Palette {
167 cream: swatch((0x00, 0x00, 0x00), 16, Color::Black),
168 terminal: swatch((0x2A, 0x29, 0x27), 235, Color::Black),
169 parchment: swatch((0x39, 0x33, 0x26), 236, Color::DarkGray),
170 glaze: swatch((0x4C, 0x43, 0x30), 238, Color::DarkGray),
171 toasted: swatch((0x63, 0x56, 0x3B), 240, Color::Gray),
172 crust: swatch((0x7E, 0x68, 0x47), 242, Color::LightCyan),
173 border: swatch((0xC0, 0x99, 0x6A), 180, Color::DarkGray),
174 tan: swatch((0xBB, 0x93, 0x61), 180, Color::DarkGray),
175 espresso: swatch((0xED, 0xC7, 0xAB), 223, Color::White),
176 cocoa: swatch((0xD4, 0x96, 0x6C), 173, Color::LightCyan),
177 latte: swatch((0xBE, 0x8A, 0x65), 137, Color::Gray),
178 ash: swatch((0x9B, 0x95, 0xA1), 246, Color::DarkGray),
179 quote: swatch((0xA1, 0x8F, 0x74), 102, Color::DarkGray),
180 caramel: swatch((0xC7, 0x77, 0x1E), 172, Color::Yellow),
181 sage: swatch((0x60, 0x97, 0x40), 107, Color::LightGreen),
182 basil: swatch((0x4A, 0xAA, 0x59), 71, Color::LightGreen),
183 cinnamon: swatch((0xD9, 0x82, 0x46), 173, Color::Yellow),
184 cranberry: swatch((0xF2, 0x6E, 0x61), 203, Color::LightRed),
185 honey: swatch((0xCC, 0x95, 0x2D), 178, Color::Yellow),
186 maple: swatch((0xE7, 0x8C, 0x33), 215, Color::White),
187 slate: swatch((0x57, 0x97, 0xC8), 67, Color::LightBlue),
188 plum: swatch((0xC5, 0x7A, 0xA0), 175, Color::LightMagenta),
189 terracotta: swatch((0xE6, 0x6E, 0x3A), 209, Color::LightRed),
190 allow_tint: swatch((0x82, 0x92, 0x57), 71, Color::LightGreen),
191 deny_tint: swatch((0xBF, 0x7B, 0x5F), 203, Color::LightRed),
192 neutral_tint: swatch((0x96, 0x8A, 0x71), 173, Color::Gray),
193 ink: swatch((0xC9, 0xB5, 0x9C), 180, Color::Gray),
194 pan: swatch((0x36, 0x34, 0x31), 237, Color::DarkGray),
195};
196
197#[derive(Clone, Debug)]
198pub struct Theme {
199 key: ThemeKey,
200}
201
202impl Default for Theme {
203 fn default() -> Self {
204 Self::new(ThemeKind::Default, ColorLevel::TrueColor)
205 }
206}
207
208impl Theme {
209 pub fn from_env() -> Self {
210 let requested = std::env::var("COOKIE_THEME").unwrap_or_default();
211 let no_color = std::env::var_os("NO_COLOR").is_some();
212 let term = std::env::var("TERM").unwrap_or_default();
213 Self::from_environment(&requested, no_color, &term, &env_colorterm())
214 }
215
216 pub fn with_kind_from_env(kind: ThemeKind) -> Self {
219 let no_color = std::env::var_os("NO_COLOR").is_some();
220 let term = std::env::var("TERM").unwrap_or_default();
221 Self::from_kind_environment(kind, no_color, &term, &env_colorterm())
222 }
223
224 pub fn from_environment(requested: &str, no_color: bool, term: &str, colorterm: &str) -> Self {
225 let requested_kind = match requested.to_ascii_lowercase().as_str() {
226 "auto" => ThemeKind::Default,
227 "dark" | "dark-roast" | "darkroast" => ThemeKind::Dark,
228 "mono" | "monochrome" => ThemeKind::Mono,
229 "high-contrast" | "high_contrast" | "contrast" => ThemeKind::HighContrast,
230 _ => ThemeKind::Default,
231 };
232 Self::from_kind_environment(requested_kind, no_color, term, colorterm)
233 }
234
235 pub fn from_kind_environment(
236 requested_kind: ThemeKind,
237 no_color: bool,
238 term: &str,
239 colorterm: &str,
240 ) -> Self {
241 let colors =
242 if requested_kind == ThemeKind::Mono || no_color || term.eq_ignore_ascii_case("dumb") {
243 ColorLevel::None
244 } else if requested_kind == ThemeKind::HighContrast {
245 ColorLevel::Ansi16
246 } else if colorterm.eq_ignore_ascii_case("truecolor")
247 || colorterm.eq_ignore_ascii_case("24bit")
248 {
249 ColorLevel::TrueColor
250 } else if term.contains("256color") {
251 ColorLevel::Ansi256
252 } else {
253 ColorLevel::Ansi16
254 };
255 let kind = if colors == ColorLevel::None {
256 ThemeKind::Mono
257 } else {
258 requested_kind
259 };
260 Self::new(kind, colors)
261 }
262
263 pub const fn new(kind: ThemeKind, colors: ColorLevel) -> Self {
264 let colors = match (kind, colors) {
265 (ThemeKind::Mono, _) | (ThemeKind::HighContrast, ColorLevel::None) => ColorLevel::None,
266 (ThemeKind::HighContrast, _) => ColorLevel::Ansi16,
267 (ThemeKind::Default | ThemeKind::Dark, colors) => colors,
268 };
269 Self {
270 key: ThemeKey { kind, colors },
271 }
272 }
273
274 pub const fn key(&self) -> ThemeKey {
275 self.key
276 }
277
278 const fn palette(&self) -> &'static Palette {
279 match self.key.kind {
280 ThemeKind::Dark => &DARK,
281 ThemeKind::Default | ThemeKind::Mono | ThemeKind::HighContrast => &LIGHT,
282 }
283 }
284
285 const fn is_bakery_palette(&self) -> bool {
286 matches!(self.key.kind, ThemeKind::Default | ThemeKind::Dark)
287 }
288
289 pub fn surface(&self) -> Style {
292 if !self.is_bakery_palette() {
293 return Style::default();
294 }
295 let palette = self.palette();
296 match self.key.colors {
297 ColorLevel::None => Style::default(),
298 ColorLevel::Ansi16 => Style::default()
299 .fg(palette.espresso.ansi16)
300 .bg(palette.cream.ansi16),
301 ColorLevel::Ansi256 => Style::default()
302 .fg(Color::Indexed(palette.espresso.ansi256))
303 .bg(Color::Indexed(palette.cream.ansi256)),
304 ColorLevel::TrueColor => Style::default()
305 .fg(Color::Rgb(
306 palette.espresso.rgb.0,
307 palette.espresso.rgb.1,
308 palette.espresso.rgb.2,
309 ))
310 .bg(Color::Rgb(
311 palette.cream.rgb.0,
312 palette.cream.rgb.1,
313 palette.cream.rgb.2,
314 )),
315 }
316 }
317
318 pub fn panel(&self) -> Style {
323 self.background_color(self.palette().cream, None)
324 .map_or_else(Style::default, |background| Style::default().bg(background))
325 }
326
327 pub fn panel_border(&self) -> Style {
330 self.semantic(self.palette().border, Color::White, Modifier::DIM)
331 }
332
333 pub fn selected(&self) -> Style {
337 let style = Style::default().add_modifier(Modifier::BOLD);
338 match self.key.colors {
339 ColorLevel::None => Style::default().add_modifier(Modifier::REVERSED),
340 _ if self.key.kind == ThemeKind::HighContrast => {
341 style.fg(Color::Black).bg(Color::LightYellow)
342 }
343 ColorLevel::Ansi16 => style.fg(Color::Black).bg(self.palette().toasted.ansi16),
344 ColorLevel::Ansi256 | ColorLevel::TrueColor => {
345 let palette = self.palette();
346 let mut style = style;
347 if let Some(foreground) = self.swatch_color(palette.espresso) {
348 style = style.fg(foreground);
349 }
350 if let Some(background) = self.swatch_color(palette.toasted) {
351 style = style.bg(background);
352 }
353 style
354 }
355 }
356 }
357
358 pub fn selected_overlay(&self) -> Style {
361 let selected = self.selected();
362 let mut overlay = selected
363 .bg
364 .map_or_else(Style::default, |background| Style::default().bg(background));
365 if selected.add_modifier.contains(Modifier::REVERSED) {
366 overlay = overlay.add_modifier(Modifier::REVERSED);
367 }
368 overlay
369 }
370
371 pub fn text_selection(&self) -> Style {
381 match self.key.colors {
382 ColorLevel::None => Style::default().add_modifier(Modifier::REVERSED | Modifier::BOLD),
383 _ if self.key.kind == ThemeKind::HighContrast => {
384 Style::default().fg(Color::Black).bg(Color::LightCyan)
385 }
386 ColorLevel::Ansi16 => Style::default()
387 .fg(Color::Black)
388 .bg(self.palette().crust.ansi16),
389 ColorLevel::Ansi256 | ColorLevel::TrueColor => self
390 .swatch_color(self.palette().crust)
391 .map_or_else(Style::default, |background| Style::default().bg(background)),
392 }
393 }
394
395 pub fn hover(&self) -> Style {
401 let quiet_background_only = matches!(
402 (self.key.kind, self.key.colors),
403 (
404 ThemeKind::Default | ThemeKind::Dark,
405 ColorLevel::Ansi256 | ColorLevel::TrueColor
406 )
407 );
408 let style = if quiet_background_only {
409 Style::default()
410 } else {
411 Style::default().add_modifier(Modifier::UNDERLINED)
412 };
413 self.background_color(self.palette().glaze, Some(Color::DarkGray))
414 .map_or(style, |background| style.bg(background))
415 }
416
417 pub fn hover_fill(&self) -> Style {
420 self.background_color(self.palette().glaze, Some(Color::DarkGray))
421 .map_or_else(Style::default, |background| Style::default().bg(background))
422 }
423
424 pub fn code_background(&self) -> Option<Color> {
427 if !self.is_bakery_palette() {
428 return None;
429 }
430 self.background_color(self.palette().parchment, None)
431 }
432
433 pub fn terminal_background(&self) -> Option<Color> {
436 if !self.is_bakery_palette() {
437 return None;
438 }
439 self.background_color(self.palette().terminal, None)
440 }
441
442 pub fn tool_title_background(&self) -> Option<Color> {
447 if !self.is_bakery_palette() || self.key.colors == ColorLevel::Ansi16 {
448 return None;
449 }
450 self.background_color(self.palette().pan, None)
451 }
452
453 pub fn block_hover(&self) -> Style {
455 let style = self.hover_fill().remove_modifier(Modifier::UNDERLINED);
456 if self.key.colors == ColorLevel::None {
457 style.add_modifier(Modifier::BOLD)
458 } else {
459 style
460 }
461 }
462
463 pub fn decision(&self, tone: DecisionTone, active: bool) -> Style {
467 if self.key.colors == ColorLevel::None {
468 let mut style = Style::default().add_modifier(Modifier::BOLD);
469 if active {
470 style = style.add_modifier(Modifier::REVERSED);
471 }
472 return style;
473 }
474 if self.key.kind == ThemeKind::HighContrast {
475 let foreground = match tone {
476 DecisionTone::Allow => Color::LightGreen,
477 DecisionTone::Deny => Color::LightRed,
478 DecisionTone::Neutral => Color::White,
479 };
480 let mut style = Style::default().fg(foreground).add_modifier(Modifier::BOLD);
481 if active {
482 style = style.add_modifier(Modifier::REVERSED);
483 }
484 return style;
485 }
486 if self.key.colors == ColorLevel::Ansi16 {
487 let palette = self.palette();
488 if active {
489 let background = match tone {
490 DecisionTone::Allow => palette.allow_tint.ansi16,
491 DecisionTone::Deny => palette.deny_tint.ansi16,
492 DecisionTone::Neutral => palette.neutral_tint.ansi16,
493 };
494 return Style::default()
495 .fg(Color::Black)
496 .bg(background)
497 .add_modifier(Modifier::BOLD);
498 }
499 let foreground = match tone {
500 DecisionTone::Allow => palette.basil.ansi16,
501 DecisionTone::Deny => palette.cranberry.ansi16,
502 DecisionTone::Neutral => palette.cocoa.ansi16,
503 };
504 return Style::default().fg(foreground).add_modifier(Modifier::BOLD);
505 }
506 let palette = self.palette();
507 let (foreground, tint) = match tone {
508 DecisionTone::Allow => (palette.basil, palette.allow_tint),
509 DecisionTone::Deny => (palette.cranberry, palette.deny_tint),
510 DecisionTone::Neutral => (palette.cocoa, palette.neutral_tint),
511 };
512 let mut style = Style::default().add_modifier(Modifier::BOLD);
513 let foreground = if active && self.key.kind == ThemeKind::Dark {
514 palette.cream
515 } else {
516 foreground
517 };
518 if let Some(foreground) = self.swatch_color(foreground) {
519 style = style.fg(foreground);
520 }
521 if active && let Some(background) = self.swatch_color(tint) {
522 style = style.bg(background);
523 }
524 style
525 }
526
527 pub fn body(&self) -> Style {
528 Style::default()
529 }
530
531 pub fn muted(&self) -> Style {
532 self.semantic(self.palette().latte, Color::White, Modifier::DIM)
533 }
534
535 pub fn muted_text(&self) -> Style {
538 self.semantic(self.palette().ink, Color::White, Modifier::DIM)
539 }
540
541 pub fn user(&self) -> Style {
542 self.semantic(self.palette().caramel, Color::LightCyan, Modifier::BOLD)
543 }
544
545 pub fn assistant(&self) -> Style {
546 self.semantic(self.palette().sage, Color::White, Modifier::BOLD)
547 }
548
549 pub fn thinking(&self) -> Style {
550 self.semantic(self.palette().plum, Color::LightMagenta, Modifier::ITALIC)
551 }
552
553 pub fn tool(&self) -> Style {
556 self.semantic(
557 self.palette().cinnamon,
558 Color::LightYellow,
559 self.weight_without_color(),
560 )
561 }
562
563 pub fn tool_running(&self) -> Style {
564 self.tool()
565 }
566
567 pub fn tool_success(&self) -> Style {
568 self.semantic(
569 self.palette().basil,
570 Color::LightGreen,
571 self.weight_without_color(),
572 )
573 }
574
575 fn weight_without_color(&self) -> Modifier {
577 if self.key.colors == ColorLevel::None {
578 Modifier::BOLD
579 } else {
580 Modifier::empty()
581 }
582 }
583
584 pub fn tool_failure(&self) -> Style {
585 self.error()
586 }
587
588 pub fn warning(&self) -> Style {
590 self.semantic(self.palette().honey, Color::LightYellow, Modifier::BOLD)
591 }
592
593 pub fn error(&self) -> Style {
594 self.semantic(self.palette().cranberry, Color::LightRed, Modifier::BOLD)
595 }
596
597 pub fn internal(&self) -> Style {
598 self.semantic(self.palette().ash, Color::Gray, Modifier::DIM)
599 }
600
601 pub fn heading(&self) -> Style {
602 let style = self.semantic(self.palette().maple, Color::White, Modifier::BOLD);
603 if self.key.kind == ThemeKind::HighContrast && self.key.colors != ColorLevel::None {
604 style.add_modifier(Modifier::UNDERLINED)
605 } else {
606 style
607 }
608 }
609
610 pub fn link(&self) -> Style {
611 self.semantic(
612 self.palette().slate,
613 Color::LightCyan,
614 Modifier::UNDERLINED | Modifier::BOLD,
615 )
616 }
617
618 pub fn inline_code(&self) -> Style {
632 if self.inline_code_is_coloured() {
633 return self.semantic(self.palette().cocoa, Color::Black, Modifier::empty());
634 }
635 if let Some(background) = self.code_background() {
636 return self
637 .semantic(self.palette().espresso, Color::Black, Modifier::empty())
638 .bg(background);
639 }
640 let foreground = self.semantic(self.palette().terracotta, Color::Black, Modifier::BOLD);
641 let background = match self.key.colors {
642 ColorLevel::None => None,
643 _ if self.key.kind == ThemeKind::HighContrast => Some(Color::LightYellow),
644 ColorLevel::Ansi16 | ColorLevel::Ansi256 | ColorLevel::TrueColor => None,
645 };
646 background.map_or(foreground, |background| foreground.bg(background))
647 }
648
649 pub fn inline_code_is_coloured(&self) -> bool {
652 self.is_bakery_palette()
653 && matches!(self.key.colors, ColorLevel::Ansi256 | ColorLevel::TrueColor)
654 }
655
656 pub fn inline_code_cap(&self) -> Option<Style> {
661 self.inline_code()
662 .bg
663 .map(|background| Style::default().fg(background))
664 }
665
666 pub fn code_border(&self) -> Style {
667 self.semantic(self.palette().tan, Color::White, Modifier::DIM)
668 }
669
670 pub fn code_gutter(&self) -> Style {
671 self.muted()
672 }
673
674 pub fn diff_added(&self) -> Style {
675 self.semantic(self.palette().basil, Color::LightGreen, Modifier::BOLD)
676 }
677
678 pub fn diff_removed(&self) -> Style {
679 self.semantic(self.palette().cranberry, Color::LightRed, Modifier::BOLD)
680 }
681
682 pub fn diff_hunk(&self) -> Style {
683 self.semantic(self.palette().slate, Color::LightCyan, Modifier::BOLD)
684 }
685
686 pub fn quote(&self) -> Style {
687 self.semantic(self.palette().quote, Color::LightMagenta, Modifier::ITALIC)
688 }
689
690 pub fn scrollbar_thumb(&self) -> Style {
691 self.semantic(self.palette().espresso, Color::White, Modifier::DIM)
692 }
693
694 pub fn input_border(&self, focused: bool) -> Style {
695 if focused {
696 if self.key.kind == ThemeKind::Dark && self.key.colors == ColorLevel::Ansi16 {
699 Style::default()
700 .fg(Color::LightYellow)
701 .add_modifier(Modifier::BOLD)
702 } else {
703 self.semantic(self.palette().honey, Color::LightYellow, Modifier::BOLD)
704 }
705 } else {
706 self.panel_border()
707 }
708 }
709
710 pub fn quantize_rgb(&self, red: u8, green: u8, blue: u8) -> Option<Color> {
711 match self.key.colors {
712 ColorLevel::None => None,
713 _ if self.key.kind == ThemeKind::HighContrast => {
714 Some(nearest_high_contrast(red, green, blue))
715 }
716 ColorLevel::TrueColor => Some(Color::Rgb(red, green, blue)),
717 ColorLevel::Ansi256 => Some(Color::Indexed(
718 self.palette()
719 .swatch_for_rgb((red, green, blue))
720 .map_or_else(|| rgb_to_ansi256(red, green, blue), |swatch| swatch.ansi256),
721 )),
722 ColorLevel::Ansi16 => Some(
723 self.palette()
724 .swatch_for_rgb((red, green, blue))
725 .map_or_else(|| nearest_ansi16(red, green, blue), |swatch| swatch.ansi16),
726 ),
727 }
728 }
729
730 fn swatch_color(&self, swatch: Swatch) -> Option<Color> {
731 match self.key.colors {
732 ColorLevel::None => None,
733 _ if self.key.kind == ThemeKind::HighContrast => Some(nearest_high_contrast(
734 swatch.rgb.0,
735 swatch.rgb.1,
736 swatch.rgb.2,
737 )),
738 ColorLevel::Ansi16 => Some(swatch.ansi16),
739 ColorLevel::Ansi256 => Some(Color::Indexed(swatch.ansi256)),
740 ColorLevel::TrueColor => Some(Color::Rgb(swatch.rgb.0, swatch.rgb.1, swatch.rgb.2)),
741 }
742 }
743
744 fn background_color(&self, swatch: Swatch, high_contrast: Option<Color>) -> Option<Color> {
747 match self.key.colors {
748 ColorLevel::None => None,
749 _ if self.key.kind == ThemeKind::HighContrast => high_contrast,
750 ColorLevel::Ansi16 => Some(swatch.ansi16),
751 ColorLevel::Ansi256 => Some(Color::Indexed(swatch.ansi256)),
752 ColorLevel::TrueColor => Some(Color::Rgb(swatch.rgb.0, swatch.rgb.1, swatch.rgb.2)),
753 }
754 }
755
756 fn semantic(&self, swatch: Swatch, high_contrast: Color, modifier: Modifier) -> Style {
757 let mut modifier = match self.key.kind {
758 ThemeKind::HighContrast => modifier | Modifier::BOLD,
759 ThemeKind::Default | ThemeKind::Dark | ThemeKind::Mono => modifier,
760 };
761 if self.is_bakery_palette() {
762 modifier.remove(Modifier::DIM);
763 }
764 let color = match self.key.colors {
765 ColorLevel::None => None,
766 _ if self.key.kind == ThemeKind::HighContrast => Some(high_contrast),
767 ColorLevel::Ansi16 => Some(swatch.ansi16),
768 ColorLevel::Ansi256 => Some(Color::Indexed(swatch.ansi256)),
769 ColorLevel::TrueColor => Some(Color::Rgb(swatch.rgb.0, swatch.rgb.1, swatch.rgb.2)),
770 };
771 color.map_or_else(
772 || Style::default().add_modifier(modifier),
773 |color| Style::default().fg(color).add_modifier(modifier),
774 )
775 }
776}
777
778fn env_colorterm() -> String {
782 let colorterm = std::env::var("COLORTERM").unwrap_or_default();
783 let term_program = std::env::var("TERM_PROGRAM").unwrap_or_default();
784 colorterm_with_hints(
785 colorterm,
786 std::env::var_os("WT_SESSION").is_some(),
787 &term_program,
788 )
789}
790
791fn colorterm_with_hints(colorterm: String, windows_terminal: bool, term_program: &str) -> String {
792 let known_truecolor =
793 windows_terminal || matches!(term_program, "vscode" | "iTerm.app" | "WezTerm" | "ghostty");
794 if colorterm.is_empty() && known_truecolor {
795 "truecolor".to_owned()
796 } else {
797 colorterm
798 }
799}
800
801fn nearest_high_contrast(red: u8, green: u8, blue: u8) -> Color {
802 const COLORS: [(Color, (u8, u8, u8)); 7] = [
803 (Color::LightRed, (255, 0, 0)),
804 (Color::LightGreen, (0, 255, 0)),
805 (Color::LightYellow, (255, 255, 0)),
806 (Color::LightBlue, (0, 0, 255)),
807 (Color::LightMagenta, (255, 0, 255)),
808 (Color::LightCyan, (0, 255, 255)),
809 (Color::White, (255, 255, 255)),
810 ];
811 nearest_color(red, green, blue, &COLORS)
812}
813
814fn rgb_to_ansi256(red: u8, green: u8, blue: u8) -> u8 {
815 let component = |value: u8| ((u16::from(value) * 5 + 127) / 255) as u8;
816 16 + 36 * component(red) + 6 * component(green) + component(blue)
817}
818
819fn nearest_ansi16(red: u8, green: u8, blue: u8) -> Color {
820 const COLORS: [(Color, (u8, u8, u8)); 16] = [
821 (Color::Black, (0, 0, 0)),
822 (Color::Red, (128, 0, 0)),
823 (Color::Green, (0, 128, 0)),
824 (Color::Yellow, (128, 128, 0)),
825 (Color::Blue, (0, 0, 128)),
826 (Color::Magenta, (128, 0, 128)),
827 (Color::Cyan, (0, 128, 128)),
828 (Color::Gray, (192, 192, 192)),
829 (Color::DarkGray, (128, 128, 128)),
830 (Color::LightRed, (255, 0, 0)),
831 (Color::LightGreen, (0, 255, 0)),
832 (Color::LightYellow, (255, 255, 0)),
833 (Color::LightBlue, (0, 0, 255)),
834 (Color::LightMagenta, (255, 0, 255)),
835 (Color::LightCyan, (0, 255, 255)),
836 (Color::White, (255, 255, 255)),
837 ];
838 nearest_color(red, green, blue, &COLORS)
839}
840
841fn nearest_color(red: u8, green: u8, blue: u8, colors: &[(Color, (u8, u8, u8))]) -> Color {
842 colors
843 .iter()
844 .min_by_key(|(_, (candidate_red, candidate_green, candidate_blue))| {
845 let red = i32::from(red) - i32::from(*candidate_red);
846 let green = i32::from(green) - i32::from(*candidate_green);
847 let blue = i32::from(blue) - i32::from(*candidate_blue);
848 red * red + green * green + blue * blue
849 })
850 .map_or(Color::White, |(color, _)| *color)
851}
852
853#[cfg(test)]
854mod tests;