use gtk::gdk;
use vte::prelude::*;
pub const THEME_NAMES: &[&str] = &[
"xfce",
"xterm",
"green-on-black",
"white-on-black",
"dark-pastels",
"solarized-dark",
"solarized-light",
"black-on-white",
];
const XFCE_PALETTE: [&str; 16] = [
"#000000", "#aa0000", "#00aa00", "#aa5500", "#0000aa", "#aa00aa", "#00aaaa", "#aaaaaa",
"#555555", "#ff5555", "#55ff55", "#ffff55", "#5555ff", "#ff55ff", "#55ffff", "#ffffff",
];
const XTERM_PALETTE: [&str; 16] = [
"#000000", "#cd0000", "#00cd00", "#cdcd00", "#0000cd", "#cd00cd", "#00cdcd", "#e5e5e5",
"#7f7f7f", "#ff0000", "#00ff00", "#ffff00", "#5c5cff", "#ff00ff", "#00ffff", "#ffffff",
];
const SOLARIZED_PALETTE: [&str; 16] = [
"#073642", "#dc322f", "#859900", "#b58900", "#268bd2", "#d33682", "#2aa198", "#eee8d5",
"#002b36", "#cb4b16", "#586e75", "#657b83", "#839496", "#6c71c4", "#93a1a1", "#fdf6e3",
];
const DARK_PASTELS_PALETTE: [&str; 16] = [
"#3f3f3f", "#705050", "#60b48a", "#dfaf8f", "#9ab8d7", "#dc8cc3", "#8cd0d3", "#dcdcdc",
"#709080", "#dca3a3", "#72d5a3", "#f0dfaf", "#94bff3", "#ec93d3", "#93e0e3", "#ffffff",
];
#[derive(Debug, Clone)]
pub struct TerminalTheme {
foreground: gdk::RGBA,
background: gdk::RGBA,
cursor: Option<gdk::RGBA>,
palette: Vec<gdk::RGBA>,
}
impl TerminalTheme {
pub fn canonical_name(name: &str) -> Result<&'static str, String> {
let normalized = name.trim().to_ascii_lowercase();
match normalized.as_str() {
"xfce" | "default" => Ok("xfce"),
"xterm" => Ok("xterm"),
"solarized-dark" | "solarized_dark" => Ok("solarized-dark"),
"solarized-light" | "solarized_light" => Ok("solarized-light"),
"dark-pastels" | "dark_pastels" => Ok("dark-pastels"),
"green-on-black" | "green_on_black" => Ok("green-on-black"),
"black-on-white" | "black_on_white" => Ok("black-on-white"),
"white-on-black" | "white_on_black" => Ok("white-on-black"),
_ => Err(format!(
"unknown theme '{name}'. Available themes: {}",
THEME_NAMES.join(", ")
)),
}
}
pub fn named(name: &str) -> Result<Self, String> {
match Self::canonical_name(name)? {
"xfce" => Ok(Self::scheme("#ffffff", "#000000", None, &XFCE_PALETTE)),
"xterm" => Ok(Self::scheme("#ffffff", "#000000", None, &XTERM_PALETTE)),
"solarized-dark" => Ok(Self::scheme(
"#839496",
"#002b36",
Some("#93a1a1"),
&SOLARIZED_PALETTE,
)),
"solarized-light" => Ok(Self::scheme(
"#073642",
"#fdf6e3",
Some("#073642"),
&SOLARIZED_PALETTE,
)),
"dark-pastels" => Ok(Self::scheme(
"#dcdcdc",
"#2c2c2c",
Some("#dcdcdc"),
&DARK_PASTELS_PALETTE,
)),
"green-on-black" => Ok(Self::scheme("#17f018", "#000000", None, &XFCE_PALETTE)),
"black-on-white" => Ok(Self::scheme("#000000", "#ffffff", None, &XTERM_PALETTE)),
"white-on-black" => Ok(Self::scheme("#ffffff", "#000000", None, &XFCE_PALETTE)),
_ => unreachable!("canonical theme names are exhaustive"),
}
}
pub fn parse_color(value: &str) -> Result<gdk::RGBA, String> {
gdk::RGBA::parse(value).map_err(|_| format!("invalid color '{value}'"))
}
pub fn apply_overrides(
&mut self,
foreground: Option<&str>,
background: Option<&str>,
cursor: Option<&str>,
palette: Option<&[String]>,
) -> Result<(), String> {
if let Some(foreground) = foreground {
self.foreground = Self::parse_color(foreground)?;
}
if let Some(background) = background {
self.background = Self::parse_color(background)?;
}
if let Some(cursor) = cursor {
self.cursor = if cursor.trim().is_empty() {
None
} else {
Some(Self::parse_color(cursor)?)
};
}
if let Some(palette) = palette {
if palette.len() != 16 {
return Err("theme.palette must contain exactly 16 colors".to_string());
}
self.palette = palette
.iter()
.map(|color| Self::parse_color(color))
.collect::<Result<Vec<_>, _>>()?;
}
Ok(())
}
pub fn with_background_alpha(&self, alpha: f64) -> Self {
let mut theme = self.clone();
theme.background.set_alpha(alpha as f32);
theme
}
pub fn background_color(&self) -> gdk::RGBA {
let mut color = self.background;
color.set_alpha(1.0);
color
}
fn scheme(foreground: &str, background: &str, cursor: Option<&str>, palette: &[&str]) -> Self {
Self {
foreground: rgba(foreground),
background: rgba(background),
cursor: cursor.map(rgba),
palette: palette.iter().map(|color| rgba(color)).collect(),
}
}
pub fn apply_to(&self, terminal: &vte::Terminal) {
let palette: Vec<&gdk::RGBA> = self.palette.iter().collect();
terminal.set_colors(Some(&self.foreground), Some(&self.background), &palette);
terminal.set_color_cursor(self.cursor.as_ref());
}
}
fn rgba(value: &str) -> gdk::RGBA {
gdk::RGBA::parse(value).unwrap_or_else(|_| panic!("invalid color literal: {value}"))
}