use rust_embed::RustEmbed;
use std::fs;
use std::path::PathBuf;
use ratatui::style::Color;
use syntect::highlighting::Theme;
#[derive(RustEmbed)]
#[folder = "themes/"]
pub struct BundledThemes;
pub struct Config;
impl Config {
pub fn get_base_dir() -> Option<PathBuf> {
dirs::home_dir().map(|p| p.join(".xtop"))
}
pub fn get_theme_dir() -> Option<PathBuf> {
Self::get_base_dir().map(|p| {
let theme_path = p.join("themes");
let _ = fs::create_dir_all(&theme_path);
theme_path
})
}
pub fn initialize_themes() -> std::io::Result<()> {
if let Some(theme_dir) = Self::get_theme_dir() {
if fs::read_dir(&theme_dir)?.next().is_none() {
for file in BundledThemes::iter() {
if let Some(embedded_file) = BundledThemes::get(&file) {
let path = theme_dir.join(file.as_ref());
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(path, embedded_file.data)?;
}
}
}
}
Ok(())
}
pub fn get_config_path() -> Option<PathBuf> {
Self::get_base_dir().map(|p| p.join("xtoprc"))
}
pub fn load_config() -> String {
let mut theme = String::from("Default-Dark");
if let Some(path) = Self::get_config_path() {
if let Ok(content) = fs::read_to_string(path) {
for line in content.lines() {
let parts: Vec<&str> = line.splitn(2, '=').collect();
if parts.len() == 2 {
match parts[0].trim() {
"theme" => theme = parts[1].trim().to_string(),
_ => {}
}
}
}
}
}
theme
}
pub fn save_config(theme: &str) {
if let Some(path) = Self::get_config_path() {
let content = format!("theme={}\n", theme);
let _ = fs::write(path, content);
}
}
}
#[derive(Clone, Copy)]
pub struct UiColors {
pub bg: Color,
pub fg: Color,
pub menu_bg: Color,
pub selected_bg: Color,
pub accent: Color,
pub title: Color,
pub is_dark: bool,
}
impl UiColors {
pub fn from_theme(theme: &Theme) -> Self {
let raw_bg = theme.settings.background.unwrap_or(syntect::highlighting::Color { r: 0, g: 0, b: 0, a: 255 });
let raw_fg = theme.settings.foreground.unwrap_or(syntect::highlighting::Color { r: 255, g: 255, b: 255, a: 255 });
let bg = Color::Rgb(raw_bg.r, raw_bg.g, raw_bg.b);
let fg = Color::Rgb(raw_fg.r, raw_fg.g, raw_fg.b);
let is_dark = (raw_bg.r as u32 + raw_bg.g as u32 + raw_bg.b as u32) < 384;
let ui_bg = if is_dark {
Color::Rgb(raw_bg.r.saturating_add(20), raw_bg.g.saturating_add(20), raw_bg.b.saturating_add(20))
} else {
Color::Rgb(raw_bg.r.saturating_sub(20), raw_bg.g.saturating_sub(20), raw_bg.b.saturating_sub(20))
};
let selected_bg = if raw_bg.r < 128 {
Color::Rgb(raw_bg.r.saturating_add(40), raw_bg.g.saturating_add(40), raw_bg.b.saturating_add(40))
} else {
Color::Rgb(raw_bg.r.saturating_sub(40), raw_bg.g.saturating_sub(40), raw_bg.b.saturating_sub(40))
};
let get_theme_color = |keys: &[&str]| -> Option<Color> {
for item in &theme.scopes {
let scope_str = format!("{:?}", item.scope).to_lowercase();
for key in keys {
if scope_str.contains(key) {
if let Some(c) = item.style.foreground {
return Some(Color::Rgb(c.r, c.g, c.b));
}
}
}
}
None
};
let mut accent = get_theme_color(&["entity.name.function", "variable"])
.unwrap_or(if is_dark { Color::Rgb(100, 200, 255) } else { Color::Rgb(20, 100, 180) });
let mut title = get_theme_color(&["keyword", "string", "constant"])
.unwrap_or(if is_dark { Color::Rgb(200, 200, 100) } else { Color::Rgb(100, 100, 50) });
if let Some(name) = &theme.name {
let lower_name = name.to_lowercase();
if lower_name.contains("catppuccin") {
accent = if is_dark { Color::Rgb(249, 226, 175) } else { Color::Rgb(223, 142, 29) };
title = if is_dark { Color::Rgb(166, 227, 161) } else { Color::Rgb(64, 160, 43) };
} else if lower_name.contains("base16") {
accent = if is_dark { Color::Rgb(250, 188, 45) } else { Color::Rgb(200, 60, 20) };
title = if is_dark { Color::Rgb(181, 189, 104) } else { Color::Rgb(113, 140, 0) };
}
}
Self { bg, fg, menu_bg: ui_bg, selected_bg, accent, title, is_dark }
}
}