1use ratatui::style::{Color, Modifier, Style};
4
5pub fn color(name: &str) -> Color {
6 Color::Indexed(match name {
7 "red" => 160,
8 "yellow" => 226,
9 "green" => 41,
10 "cyan" => 37,
11 "blue" => 39,
12 "purple" => 141,
13 "white" => 231,
14 "black" => 234,
15 "grey" => 244,
16 _ => 39,
17 })
18}
19
20pub const RED: Color = Color::Indexed(196);
21pub const GREEN: Color = Color::Indexed(41);
22pub const GREY: Color = Color::Indexed(244);
23
24pub fn dimmed(c: Color) -> Color {
27 let Color::Indexed(code) = c else { return c };
28 let dim = match code {
29 0..=15 => {
30 if code >= 8 {
31 code - 8
32 } else {
33 code
34 }
35 }
36 232.. => code.saturating_sub(4).max(232),
37 _ => {
38 let base = code - 16;
39 let (r, g, b) = (base / 36, (base % 36) / 6, base % 6);
40 16 + 36 * r.saturating_sub(1) + 6 * g.saturating_sub(1) + b.saturating_sub(1)
41 }
42 };
43 Color::Indexed(dim)
44}
45
46pub fn tint(color: Color) -> Color {
55 let (r, g, b) = rgb_of(color);
56 let wash = |c: u8| (c / 4).saturating_add(8);
57 Color::Rgb(wash(r), wash(g), wash(b))
58}
59
60fn rgb_of(color: Color) -> (u8, u8, u8) {
62 let code = match color {
63 Color::Rgb(r, g, b) => return (r, g, b),
64 Color::Indexed(code) => code,
65 _ => return (0, 0, 0),
66 };
67 match code {
68 16..232 => {
70 let level = |c: u8| if c == 0 { 0 } else { 55 + c * 40 };
71 let base = code - 16;
72 (level(base / 36), level((base % 36) / 6), level(base % 6))
73 }
74 232.. => {
76 let v = 8 + (code - 232) * 10;
77 (v, v, v)
78 }
79 _ => (0, 0, 0),
80 }
81}
82
83pub struct Theme {
84 pub accent: Color,
85 high_contrast_selection: bool,
86 colors_disabled: bool,
87}
88
89impl Theme {
90 pub fn new(name: &str) -> Self {
91 let colors_disabled = std::env::var_os("NO_COLOR").is_some()
92 || std::env::var("TERM").is_ok_and(|term| term == "dumb");
93 let light = terminal_background_is_light();
94 Self::with_environment(name, colors_disabled, light)
95 }
96
97 pub fn with_environment(name: &str, colors_disabled: bool, light_background: bool) -> Self {
98 let accent = if colors_disabled {
99 Color::Reset
100 } else if light_background {
101 Color::Indexed(match name {
102 "red" => 124,
103 "yellow" => 136,
104 "green" => 28,
105 "cyan" => 30,
106 "blue" => 25,
107 "purple" => 91,
108 "white" => 238,
109 "black" => 16,
110 _ => 25,
111 })
112 } else {
113 color(name)
114 };
115 Self {
116 accent,
117 high_contrast_selection: colors_disabled || light_background,
118 colors_disabled,
119 }
120 }
121
122 pub fn muted_color(&self) -> Color {
123 if self.colors_disabled {
124 Color::Reset
125 } else {
126 GREY
127 }
128 }
129
130 pub fn error_color(&self) -> Color {
131 if self.colors_disabled {
132 Color::Reset
133 } else {
134 RED
135 }
136 }
137
138 pub fn success_color(&self) -> Color {
139 if self.colors_disabled {
140 Color::Reset
141 } else {
142 GREEN
143 }
144 }
145
146 pub fn selection_wash(&self) -> Color {
147 if self.colors_disabled {
148 Color::Reset
149 } else {
150 tint(self.accent)
151 }
152 }
153
154 pub fn dimmed_accent(&self) -> Color {
155 if self.colors_disabled {
156 Color::Reset
157 } else {
158 dimmed(self.accent)
159 }
160 }
161
162 pub fn selection(&self) -> Style {
165 if self.high_contrast_selection {
166 Style::new().add_modifier(Modifier::BOLD | Modifier::REVERSED)
167 } else {
168 Style::new()
169 .bg(self.selection_wash())
170 .add_modifier(Modifier::BOLD)
171 }
172 }
173
174 pub fn selection_unfocused(&self) -> Style {
178 if self.high_contrast_selection {
179 Style::new().add_modifier(Modifier::REVERSED)
180 } else {
181 Style::new().bg(self.selection_wash())
182 }
183 }
184
185 pub fn accent_text(&self) -> Style {
186 Style::new().fg(self.accent)
187 }
188
189 pub fn plain(&self) -> Style {
190 Style::new()
191 }
192}
193
194fn terminal_background_is_light() -> bool {
195 let Ok(value) = std::env::var("COLORFGBG") else {
196 return false;
197 };
198 value
199 .split([';', ':'])
200 .next_back()
201 .and_then(|value| value.parse::<u8>().ok())
202 .is_some_and(|background| matches!(background, 7 | 10..=15))
203}
204
205pub fn reduced_motion() -> bool {
206 std::env::var("MACH_REDUCED_MOTION").is_ok_and(|value| {
207 matches!(
208 value.to_ascii_lowercase().as_str(),
209 "1" | "true" | "yes" | "on"
210 )
211 })
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn no_color_and_light_terminals_use_contrast_instead_of_rgb_washes() {
220 let no_color = Theme::with_environment("cyan", true, false);
221 assert_eq!(no_color.accent, Color::Reset);
222 assert!(
223 no_color
224 .selection()
225 .add_modifier
226 .contains(Modifier::REVERSED)
227 );
228 assert_eq!(no_color.muted_color(), Color::Reset);
229 assert_eq!(no_color.error_color(), Color::Reset);
230 assert_eq!(no_color.success_color(), Color::Reset);
231 assert_eq!(no_color.selection_wash(), Color::Reset);
232 assert_eq!(no_color.dimmed_accent(), Color::Reset);
233
234 let light = Theme::with_environment("white", false, true);
235 assert_eq!(light.accent, Color::Indexed(238));
236 assert!(light.selection().add_modifier.contains(Modifier::REVERSED));
237 }
238}