1use bevy_ecs::prelude::*;
21use ratatui::style::{Color, Modifier, Style, Stylize};
22use ratatui::text::{Line, Span};
23use ratatui::widgets::{Block, Padding};
24
25const DARK: Rgb = (0x1c, 0x1c, 0x1c);
28const LIGHT: Rgb = (0xfa, 0xfa, 0xfa);
29
30const ADDED: (Rgb, f32) = ((0x3f, 0xb9, 0x50), 0.16);
32const ADDED_WORD: (Rgb, f32) = ((0x3f, 0xb9, 0x50), 0.38);
33const REMOVED: (Rgb, f32) = ((0xf8, 0x51, 0x49), 0.16);
34const REMOVED_WORD: (Rgb, f32) = ((0xf8, 0x51, 0x49), 0.38);
35const SELECTED: (Rgb, f32) = ((0xd2, 0x99, 0x22), 0.30);
36const CALL_RUNNING: (Rgb, f32) = ((0xd2, 0x99, 0x22), 0.14);
37const CALL_DONE: (Rgb, f32) = ((0x3f, 0xb9, 0x50), 0.10);
38const CALL_FAILED: (Rgb, f32) = ((0xf8, 0x51, 0x49), 0.24);
39const NOTE: (Rgb, f32) = ((0x58, 0xa6, 0xff), 0.16);
40const IMPORTANT: (Rgb, f32) = ((0xa3, 0x71, 0xf7), 0.18);
41const USER: f32 = 0.12;
44const TOOL: f32 = 0.06;
45const PANEL: f32 = 0.04;
49const INPUT: f32 = 0.17;
50const RAISED: f32 = 0.21;
51const CALL_WAITING: f32 = 0.10;
54const CODE: f32 = 0.08;
56
57const DARK_256: Theme = indexed(
61 DARK,
62 [
63 238, 236, 22, 28, 52, 88, 58, 235, 240, 237, 94, 23, 89, 239, 234, 17, 53,
64 ],
65);
66const LIGHT_256: Theme = indexed(
67 LIGHT,
68 [
69 252, 254, 194, 157, 224, 217, 230, 255, 248, 251, 223, 193, 218, 250, 253, 189, 225,
70 ],
71);
72
73type Rgb = (u8, u8, u8);
74
75#[derive(Resource, Clone, Copy, Debug, PartialEq, Eq, Hash)]
79pub struct Theme {
80 pub user: Color,
82 pub tool: Color,
85 pub added: Color,
87 pub added_word: Color,
89 pub removed: Color,
91 pub removed_word: Color,
93 pub selected: Color,
95 pub panel_surface: Color,
97 pub raised_surface: Color,
99 pub input: Color,
101 pub call_waiting: Color,
103 pub call_running: Color,
105 pub call_done: Color,
107 pub call_failed: Color,
109 pub code: Color,
111 pub note: Color,
116 pub important: Color,
118 pub plain: bool,
121 background: Rgb,
123 truecolor: bool,
125}
126
127impl Default for Theme {
128 fn default() -> Self {
131 Self::on(DARK, truecolor())
132 }
133}
134
135impl Theme {
136 pub fn on(background: Rgb, truecolor: bool) -> Self {
139 let dark = lightness(background) < 0.5;
140 if !truecolor {
141 return if dark { DARK_256 } else { LIGHT_256 };
142 }
143 let contrast = if dark { (0xff, 0xff, 0xff) } else { (0, 0, 0) };
144 let lean = |(to, by): (Rgb, f32)| {
145 let (r, g, b) = mix(background, to, by);
146 Color::Rgb(r, g, b)
147 };
148 Self {
149 user: lean((contrast, USER)),
150 tool: lean((contrast, TOOL)),
151 added: lean(ADDED),
152 added_word: lean(ADDED_WORD),
153 removed: lean(REMOVED),
154 removed_word: lean(REMOVED_WORD),
155 selected: lean(SELECTED),
156 panel_surface: lean((contrast, PANEL)),
157 raised_surface: lean((contrast, RAISED)),
158 input: lean((contrast, INPUT)),
159 call_waiting: lean((contrast, CALL_WAITING)),
160 call_running: lean(CALL_RUNNING),
161 call_done: lean(CALL_DONE),
162 call_failed: lean(CALL_FAILED),
163 code: lean((contrast, CODE)),
164 note: lean(NOTE),
165 important: lean(IMPORTANT),
166 plain: false,
167 background,
168 truecolor,
169 }
170 }
171
172 pub fn shade(&self, by: f32) -> Color {
176 let toward = match lightness(self.background) < 0.5 {
177 true => (0xff, 0xff, 0xff),
178 false => (0, 0, 0),
179 };
180 let (r, g, b) = mix(self.background, toward, by.clamp(0.0, 1.0));
181 if self.truecolor {
182 return Color::Rgb(r, g, b);
183 }
184 let level = lightness((r, g, b)) * 255.0;
186 let step = ((level - 8.0) / 10.0).round().clamp(0.0, 23.0) as u8;
187 Color::Indexed(232 + step)
188 }
189
190 pub fn without_color(self) -> Self {
192 Self {
193 plain: true,
194 ..self
195 }
196 }
197
198 pub fn selection(&self) -> Style {
201 match self.plain {
202 true => Style::new().add_modifier(Modifier::REVERSED),
203 false => Style::new().bg(self.selected),
204 }
205 }
206
207 pub fn changed_word(&self, tint: Color) -> Style {
211 match self.plain {
212 true => Style::new().add_modifier(Modifier::UNDERLINED),
213 false => Style::new().bg(tint),
214 }
215 }
216
217 pub fn panel<'a>(&self, title: impl Into<Line<'a>>) -> Block<'a> {
222 surface(self.panel_surface, title.into(), self.plain)
223 }
224
225 pub fn raised<'a>(&self, title: impl Into<Line<'a>>) -> Block<'a> {
228 surface(self.raised_surface, title.into(), self.plain)
229 }
230
231 pub(crate) fn of_terminal() -> Self {
235 if no_color() {
236 return Self::default().without_color();
237 }
238 let options = terminal_colorsaurus::QueryOptions::default();
239 match terminal_colorsaurus::background_color(options) {
240 Ok(background) => Self::on(background.scale_to_8bit(), truecolor()),
241 Err(_) => Self::default(),
242 }
243 }
244}
245
246fn surface(color: Color, mut title: Line<'_>, plain: bool) -> Block<'_> {
249 title.spans.insert(0, Span::from(" "));
250 title.spans.push(Span::from(" "));
251 let title = match plain {
252 true => title.bold().reversed(),
253 false => title.bold(),
254 };
255 Block::new()
256 .bg(color)
257 .title(title)
258 .padding(Padding::new(1, 1, 0, 1))
259}
260
261fn no_color() -> bool {
263 std::env::var_os("NO_COLOR").is_some_and(|value| !value.is_empty())
264}
265
266fn truecolor() -> bool {
268 std::env::var("COLORTERM").is_ok_and(|term| term == "truecolor" || term == "24bit")
269}
270
271fn mix(from: Rgb, to: Rgb, by: f32) -> Rgb {
273 let channel = |from: u8, to: u8| {
274 let mixed = f32::from(from) + (f32::from(to) - f32::from(from)) * by;
275 mixed.round().clamp(0.0, 255.0) as u8
276 };
277 (
278 channel(from.0, to.0),
279 channel(from.1, to.1),
280 channel(from.2, to.2),
281 )
282}
283
284fn lightness((r, g, b): Rgb) -> f32 {
286 (0.2126 * f32::from(r) + 0.7152 * f32::from(g) + 0.0722 * f32::from(b)) / 255.0
287}
288
289const fn indexed(
291 background: Rgb,
292 [
293 user,
294 tool,
295 added,
296 added_word,
297 removed,
298 removed_word,
299 selected,
300 panel_surface,
301 raised_surface,
302 call_waiting,
303 call_running,
304 call_done,
305 call_failed,
306 input,
307 code,
308 note,
309 important,
310 ]: [u8; 17],
311) -> Theme {
312 Theme {
313 user: Color::Indexed(user),
314 tool: Color::Indexed(tool),
315 added: Color::Indexed(added),
316 added_word: Color::Indexed(added_word),
317 removed: Color::Indexed(removed),
318 removed_word: Color::Indexed(removed_word),
319 selected: Color::Indexed(selected),
320 panel_surface: Color::Indexed(panel_surface),
321 raised_surface: Color::Indexed(raised_surface),
322 call_waiting: Color::Indexed(call_waiting),
323 call_running: Color::Indexed(call_running),
324 call_done: Color::Indexed(call_done),
325 call_failed: Color::Indexed(call_failed),
326 input: Color::Indexed(input),
327 code: Color::Indexed(code),
328 note: Color::Indexed(note),
329 important: Color::Indexed(important),
330 plain: false,
331 background,
332 truecolor: false,
333 }
334}
335
336#[cfg(test)]
337mod tests;