include!(concat!(env!("OUT_DIR"), "/builtin_themes.rs"));
use std::path::Path;
use ratatui::style::Color;
use serde::Deserialize;
use crate::errors::{ConfigError, ConfigResult};
#[derive(Debug, Clone)]
pub struct Theme {
pub name: String,
pub colors: Colors,
}
#[derive(Debug, Clone, Copy)]
pub struct Colors {
pub background: Color,
pub surface: Color,
pub overlay: Color,
pub text: Color,
pub subtext: Color,
pub muted: Color,
pub border: Color,
pub accent: Color,
pub accent_alt: Color,
pub success: Color,
pub warning: Color,
pub danger: Color,
pub download: Color,
pub upload: Color,
pub latency: Color,
}
impl Colors {
#[must_use]
pub fn speed_color(&self, bps: f64) -> Color {
let mbps = bps / 1_000_000.0;
if mbps >= 100.0 {
self.success
} else if mbps >= 25.0 {
self.warning
} else {
self.danger
}
}
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct RawTheme {
name: String,
colors: RawColors,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct RawColors {
background: String,
surface: String,
overlay: String,
text: String,
subtext: String,
muted: String,
border: String,
accent: String,
accent_alt: String,
success: String,
warning: String,
danger: String,
download: String,
upload: String,
latency: String,
}
impl Theme {
pub fn from_toml(source: &str) -> ConfigResult<Self> {
let raw: RawTheme =
toml::from_str(source).map_err(|err| ConfigError::InvalidTheme(Box::new(err)))?;
Ok(Self {
name: raw.name,
colors: Colors {
background: parse_hex(&raw.colors.background)?,
surface: parse_hex(&raw.colors.surface)?,
overlay: parse_hex(&raw.colors.overlay)?,
text: parse_hex(&raw.colors.text)?,
subtext: parse_hex(&raw.colors.subtext)?,
muted: parse_hex(&raw.colors.muted)?,
border: parse_hex(&raw.colors.border)?,
accent: parse_hex(&raw.colors.accent)?,
accent_alt: parse_hex(&raw.colors.accent_alt)?,
success: parse_hex(&raw.colors.success)?,
warning: parse_hex(&raw.colors.warning)?,
danger: parse_hex(&raw.colors.danger)?,
download: parse_hex(&raw.colors.download)?,
upload: parse_hex(&raw.colors.upload)?,
latency: parse_hex(&raw.colors.latency)?,
},
})
}
pub fn builtin() -> ConfigResult<Vec<Self>> {
BUILTIN_THEME_SOURCES
.iter()
.map(|src| Self::from_toml(src))
.collect()
}
pub fn load_all(user_dir: Option<&Path>) -> ConfigResult<Vec<Self>> {
let mut themes = Self::builtin()?;
if let Some(dir) = user_dir {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "toml") {
if let Ok(source) = std::fs::read_to_string(&path) {
if let Ok(theme) = Self::from_toml(&source) {
themes.push(theme);
}
}
}
}
}
}
Ok(themes)
}
}
pub fn parse_hex(value: &str) -> ConfigResult<Color> {
let hex = value.trim().trim_start_matches('#');
if hex.len() != 6 || !hex.bytes().all(|b| b.is_ascii_hexdigit()) {
return Err(ConfigError::InvalidColor(value.to_owned()));
}
let component = |range: std::ops::Range<usize>| -> ConfigResult<u8> {
u8::from_str_radix(&hex[range], 16).map_err(|_| ConfigError::InvalidColor(value.to_owned()))
};
Ok(Color::Rgb(
component(0..2)?,
component(2..4)?,
component(4..6)?,
))
}
impl Theme {
#[must_use]
pub fn fallback() -> Self {
let gray = Color::Rgb(160, 160, 170);
Self {
name: "Fallback".to_owned(),
colors: Colors {
background: Color::Rgb(20, 20, 26),
surface: Color::Rgb(30, 30, 38),
overlay: Color::Rgb(40, 40, 50),
text: Color::Rgb(220, 220, 228),
subtext: gray,
muted: Color::Rgb(100, 100, 112),
border: Color::Rgb(60, 60, 72),
accent: Color::Rgb(122, 162, 247),
accent_alt: Color::Rgb(187, 154, 247),
success: Color::Rgb(158, 206, 106),
warning: Color::Rgb(224, 175, 104),
danger: Color::Rgb(247, 118, 142),
download: Color::Rgb(125, 207, 255),
upload: Color::Rgb(187, 154, 247),
latency: Color::Rgb(158, 206, 106),
},
}
}
#[must_use]
pub fn without_colors(&self) -> Self {
let c = Color::Reset;
Self {
name: self.name.clone(),
colors: Colors {
background: c,
surface: c,
overlay: c,
text: c,
subtext: c,
muted: c,
border: c,
accent: c,
accent_alt: c,
success: c,
warning: c,
danger: c,
download: c,
upload: c,
latency: c,
},
}
}
}
#[must_use]
pub fn blend(a: Color, b: Color, t: f64) -> Color {
match (a, b) {
(Color::Rgb(r1, g1, b1), Color::Rgb(r2, g2, b2)) => {
let t = t.clamp(0.0, 1.0);
let mix = |x: u8, y: u8| -> u8 {
(f64::from(x) + (f64::from(y) - f64::from(x)) * t).round() as u8
};
Color::Rgb(mix(r1, r2), mix(g1, g2), mix(b1, b2))
}
_ => a,
}
}