use bevy_ecs::prelude::*;
use ratatui::style::{Color, Modifier, Style, Stylize};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Padding};
const DARK: Rgb = (0x1c, 0x1c, 0x1c);
const LIGHT: Rgb = (0xfa, 0xfa, 0xfa);
const ADDED: (Rgb, f32) = ((0x3f, 0xb9, 0x50), 0.16);
const ADDED_WORD: (Rgb, f32) = ((0x3f, 0xb9, 0x50), 0.38);
const REMOVED: (Rgb, f32) = ((0xf8, 0x51, 0x49), 0.16);
const REMOVED_WORD: (Rgb, f32) = ((0xf8, 0x51, 0x49), 0.38);
const SELECTED: (Rgb, f32) = ((0xd2, 0x99, 0x22), 0.30);
const CALL_RUNNING: (Rgb, f32) = ((0xd2, 0x99, 0x22), 0.14);
const CALL_DONE: (Rgb, f32) = ((0x3f, 0xb9, 0x50), 0.10);
const CALL_FAILED: (Rgb, f32) = ((0xf8, 0x51, 0x49), 0.24);
const NOTE: (Rgb, f32) = ((0x58, 0xa6, 0xff), 0.16);
const IMPORTANT: (Rgb, f32) = ((0xa3, 0x71, 0xf7), 0.18);
const USER: f32 = 0.12;
const TOOL: f32 = 0.06;
const PANEL: f32 = 0.04;
const INPUT: f32 = 0.17;
const RAISED: f32 = 0.21;
const CALL_WAITING: f32 = 0.10;
const CODE: f32 = 0.08;
const DARK_256: Theme = indexed(
DARK,
[
238, 236, 22, 28, 52, 88, 58, 235, 240, 237, 94, 23, 89, 239, 234, 17, 53,
],
);
const LIGHT_256: Theme = indexed(
LIGHT,
[
252, 254, 194, 157, 224, 217, 230, 255, 248, 251, 223, 193, 218, 250, 253, 189, 225,
],
);
type Rgb = (u8, u8, u8);
#[derive(Resource, Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Theme {
pub user: Color,
pub tool: Color,
pub added: Color,
pub added_word: Color,
pub removed: Color,
pub removed_word: Color,
pub selected: Color,
pub panel_surface: Color,
pub raised_surface: Color,
pub input: Color,
pub call_waiting: Color,
pub call_running: Color,
pub call_done: Color,
pub call_failed: Color,
pub code: Color,
pub note: Color,
pub important: Color,
pub plain: bool,
background: Rgb,
truecolor: bool,
}
impl Default for Theme {
fn default() -> Self {
Self::on(DARK, truecolor())
}
}
impl Theme {
pub fn on(background: Rgb, truecolor: bool) -> Self {
let dark = lightness(background) < 0.5;
if !truecolor {
return if dark { DARK_256 } else { LIGHT_256 };
}
let contrast = if dark { (0xff, 0xff, 0xff) } else { (0, 0, 0) };
let lean = |(to, by): (Rgb, f32)| {
let (r, g, b) = mix(background, to, by);
Color::Rgb(r, g, b)
};
Self {
user: lean((contrast, USER)),
tool: lean((contrast, TOOL)),
added: lean(ADDED),
added_word: lean(ADDED_WORD),
removed: lean(REMOVED),
removed_word: lean(REMOVED_WORD),
selected: lean(SELECTED),
panel_surface: lean((contrast, PANEL)),
raised_surface: lean((contrast, RAISED)),
input: lean((contrast, INPUT)),
call_waiting: lean((contrast, CALL_WAITING)),
call_running: lean(CALL_RUNNING),
call_done: lean(CALL_DONE),
call_failed: lean(CALL_FAILED),
code: lean((contrast, CODE)),
note: lean(NOTE),
important: lean(IMPORTANT),
plain: false,
background,
truecolor,
}
}
pub fn shade(&self, by: f32) -> Color {
let toward = match lightness(self.background) < 0.5 {
true => (0xff, 0xff, 0xff),
false => (0, 0, 0),
};
let (r, g, b) = mix(self.background, toward, by.clamp(0.0, 1.0));
if self.truecolor {
return Color::Rgb(r, g, b);
}
let level = lightness((r, g, b)) * 255.0;
let step = ((level - 8.0) / 10.0).round().clamp(0.0, 23.0) as u8;
Color::Indexed(232 + step)
}
pub fn without_color(self) -> Self {
Self {
plain: true,
..self
}
}
pub fn selection(&self) -> Style {
match self.plain {
true => Style::new().add_modifier(Modifier::REVERSED),
false => Style::new().bg(self.selected),
}
}
pub fn changed_word(&self, tint: Color) -> Style {
match self.plain {
true => Style::new().add_modifier(Modifier::UNDERLINED),
false => Style::new().bg(tint),
}
}
pub fn panel<'a>(&self, title: impl Into<Line<'a>>) -> Block<'a> {
surface(self.panel_surface, title.into(), self.plain)
}
pub fn raised<'a>(&self, title: impl Into<Line<'a>>) -> Block<'a> {
surface(self.raised_surface, title.into(), self.plain)
}
pub(crate) fn of_terminal() -> Self {
if no_color() {
return Self::default().without_color();
}
let options = terminal_colorsaurus::QueryOptions::default();
match terminal_colorsaurus::background_color(options) {
Ok(background) => Self::on(background.scale_to_8bit(), truecolor()),
Err(_) => Self::default(),
}
}
}
fn surface(color: Color, mut title: Line<'_>, plain: bool) -> Block<'_> {
title.spans.insert(0, Span::from(" "));
title.spans.push(Span::from(" "));
let title = match plain {
true => title.bold().reversed(),
false => title.bold(),
};
Block::new()
.bg(color)
.title(title)
.padding(Padding::new(1, 1, 0, 1))
}
fn no_color() -> bool {
std::env::var_os("NO_COLOR").is_some_and(|value| !value.is_empty())
}
fn truecolor() -> bool {
std::env::var("COLORTERM").is_ok_and(|term| term == "truecolor" || term == "24bit")
}
fn mix(from: Rgb, to: Rgb, by: f32) -> Rgb {
let channel = |from: u8, to: u8| {
let mixed = f32::from(from) + (f32::from(to) - f32::from(from)) * by;
mixed.round().clamp(0.0, 255.0) as u8
};
(
channel(from.0, to.0),
channel(from.1, to.1),
channel(from.2, to.2),
)
}
fn lightness((r, g, b): Rgb) -> f32 {
(0.2126 * f32::from(r) + 0.7152 * f32::from(g) + 0.0722 * f32::from(b)) / 255.0
}
const fn indexed(
background: Rgb,
[
user,
tool,
added,
added_word,
removed,
removed_word,
selected,
panel_surface,
raised_surface,
call_waiting,
call_running,
call_done,
call_failed,
input,
code,
note,
important,
]: [u8; 17],
) -> Theme {
Theme {
user: Color::Indexed(user),
tool: Color::Indexed(tool),
added: Color::Indexed(added),
added_word: Color::Indexed(added_word),
removed: Color::Indexed(removed),
removed_word: Color::Indexed(removed_word),
selected: Color::Indexed(selected),
panel_surface: Color::Indexed(panel_surface),
raised_surface: Color::Indexed(raised_surface),
call_waiting: Color::Indexed(call_waiting),
call_running: Color::Indexed(call_running),
call_done: Color::Indexed(call_done),
call_failed: Color::Indexed(call_failed),
input: Color::Indexed(input),
code: Color::Indexed(code),
note: Color::Indexed(note),
important: Color::Indexed(important),
plain: false,
background,
truecolor: false,
}
}
#[cfg(test)]
mod tests;