#[cfg(feature = "fs")]
use anyhow::{Context, Result};
use ratatui::style::Color;
#[cfg(feature = "fs")]
use std::path::PathBuf;
#[cfg(feature = "fs")]
use tca_types::BuiltinTheme;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Meta {
pub name: String,
pub author: String,
pub dark: bool,
}
impl Default for Meta {
fn default() -> Self {
Self {
name: "Unnamed Theme".to_string(),
author: String::new(),
dark: false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Ansi {
pub black: Color,
pub red: Color,
pub green: Color,
pub yellow: Color,
pub blue: Color,
pub magenta: Color,
pub cyan: Color,
pub white: Color,
pub bright_black: Color,
pub bright_red: Color,
pub bright_green: Color,
pub bright_yellow: Color,
pub bright_blue: Color,
pub bright_magenta: Color,
pub bright_cyan: Color,
pub bright_white: Color,
}
impl Ansi {
pub fn get(&self, key: &str) -> Option<Color> {
match key {
"black" => Some(self.black),
"red" => Some(self.red),
"green" => Some(self.green),
"yellow" => Some(self.yellow),
"blue" => Some(self.blue),
"magenta" => Some(self.magenta),
"cyan" => Some(self.cyan),
"white" => Some(self.white),
"bright_black" => Some(self.bright_black),
"bright_red" => Some(self.bright_red),
"bright_green" => Some(self.bright_green),
"bright_yellow" => Some(self.bright_yellow),
"bright_blue" => Some(self.bright_blue),
"bright_magenta" => Some(self.bright_magenta),
"bright_cyan" => Some(self.bright_cyan),
"bright_white" => Some(self.bright_white),
_ => None,
}
}
}
impl Default for Ansi {
fn default() -> Self {
Self {
black: Color::Black,
red: Color::Red,
green: Color::Green,
yellow: Color::Yellow,
blue: Color::Blue,
magenta: Color::Magenta,
cyan: Color::Cyan,
white: Color::Gray,
bright_black: Color::DarkGray,
bright_red: Color::LightRed,
bright_green: Color::LightGreen,
bright_yellow: Color::LightYellow,
bright_blue: Color::LightBlue,
bright_magenta: Color::LightMagenta,
bright_cyan: Color::LightCyan,
bright_white: Color::White,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Semantic {
pub error: Color,
pub warning: Color,
pub info: Color,
pub success: Color,
pub highlight: Color,
pub link: Color,
}
impl Default for Semantic {
fn default() -> Self {
Self {
error: Color::Red,
warning: Color::Yellow,
info: Color::Blue,
success: Color::Green,
highlight: Color::Cyan,
link: Color::Blue,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Ui {
pub bg_primary: Color,
pub bg_secondary: Color,
pub fg_primary: Color,
pub fg_secondary: Color,
pub fg_muted: Color,
pub border_primary: Color,
pub border_muted: Color,
pub cursor_primary: Color,
pub cursor_muted: Color,
pub selection_bg: Color,
pub selection_fg: Color,
}
impl Default for Ui {
fn default() -> Self {
Self {
bg_primary: Color::Black,
bg_secondary: Color::Black,
fg_primary: Color::White,
fg_secondary: Color::Gray,
fg_muted: Color::DarkGray,
border_primary: Color::White,
border_muted: Color::DarkGray,
cursor_primary: Color::White,
cursor_muted: Color::Gray,
selection_bg: Color::DarkGray,
selection_fg: Color::White,
}
}
}
#[derive(Debug, Clone)]
pub struct TcaTheme {
pub meta: Meta,
pub ansi: Ansi,
pub semantic: Semantic,
pub ui: Ui,
pub base24: [Color; 24],
}
impl TcaTheme {
#[cfg(feature = "fs")]
pub fn new(name: Option<&str>) -> Self {
TcaTheme::try_from(tca_types::Theme::from_name(name)).unwrap_or_else(|_| {
use terminal_colorsaurus::{theme_mode, QueryOptions, ThemeMode};
let builtin = match theme_mode(QueryOptions::default()).ok() {
Some(ThemeMode::Light) => BuiltinTheme::default_light(),
_ => BuiltinTheme::default(),
};
TcaTheme::try_from(builtin.theme()).expect("hardcoded default must be valid")
})
}
pub fn name_slug(&self) -> String {
heck::AsKebabCase(&self.meta.name).to_string()
}
pub fn to_filename(&self) -> String {
let mut theme_name = self.name_slug();
if !theme_name.ends_with(".yaml") {
theme_name.push_str(".yaml");
}
theme_name
}
#[cfg(feature = "fs")]
pub fn to_pathbuf(&self) -> Result<PathBuf> {
use tca_types::user_themes_path;
let mut path = user_themes_path()?;
path.push(self.to_filename());
Ok(path)
}
}
#[cfg(feature = "fs")]
impl Default for TcaTheme {
fn default() -> Self {
TcaTheme::new(None)
}
}
#[cfg(feature = "fs")]
impl TryFrom<&str> for TcaTheme {
type Error = anyhow::Error;
fn try_from(value: &str) -> Result<TcaTheme, Self::Error> {
let raw = tca_types::Theme::from_base24_str(value)?;
TcaTheme::try_from(raw)
}
}
#[cfg(feature = "fs")]
impl TryFrom<tca_types::Theme> for TcaTheme {
type Error = anyhow::Error;
fn try_from(raw: tca_types::Theme) -> Result<TcaTheme, Self::Error> {
let ansi = parse_ansi(&raw.ansi)?;
let c = |hex: &str| hex_to_color(hex).unwrap_or(Color::Reset);
let defaults = Semantic::default();
let semantic = Semantic {
error: hex_to_color(&raw.semantic.error).unwrap_or(defaults.error),
warning: hex_to_color(&raw.semantic.warning).unwrap_or(defaults.warning),
info: hex_to_color(&raw.semantic.info).unwrap_or(defaults.info),
success: hex_to_color(&raw.semantic.success).unwrap_or(defaults.success),
highlight: hex_to_color(&raw.semantic.highlight).unwrap_or(defaults.highlight),
link: hex_to_color(&raw.semantic.link).unwrap_or(defaults.link),
};
let defaults = Ui::default();
let ui = Ui {
bg_primary: hex_to_color(&raw.ui.bg.primary).unwrap_or(defaults.bg_primary),
bg_secondary: hex_to_color(&raw.ui.bg.secondary).unwrap_or(defaults.bg_secondary),
fg_primary: hex_to_color(&raw.ui.fg.primary).unwrap_or(defaults.fg_primary),
fg_secondary: hex_to_color(&raw.ui.fg.secondary).unwrap_or(defaults.fg_secondary),
fg_muted: hex_to_color(&raw.ui.fg.muted).unwrap_or(defaults.fg_muted),
border_primary: hex_to_color(&raw.ui.border.primary).unwrap_or(defaults.border_primary),
border_muted: hex_to_color(&raw.ui.border.muted).unwrap_or(defaults.border_muted),
cursor_primary: hex_to_color(&raw.ui.cursor.primary).unwrap_or(defaults.cursor_primary),
cursor_muted: hex_to_color(&raw.ui.cursor.muted).unwrap_or(defaults.cursor_muted),
selection_bg: hex_to_color(&raw.ui.selection.bg).unwrap_or(defaults.selection_bg),
selection_fg: hex_to_color(&raw.ui.selection.fg).unwrap_or(defaults.selection_fg),
};
let s = &raw.base24;
let base24 = [
c(&s.base00),
c(&s.base01),
c(&s.base02),
c(&s.base03),
c(&s.base04),
c(&s.base05),
c(&s.base06),
c(&s.base07),
c(&s.base08),
c(&s.base09),
c(&s.base0a),
c(&s.base0b),
c(&s.base0c),
c(&s.base0d),
c(&s.base0e),
c(&s.base0f),
c(&s.base10),
c(&s.base11),
c(&s.base12),
c(&s.base13),
c(&s.base14),
c(&s.base15),
c(&s.base16),
c(&s.base17),
];
let meta = Meta {
name: raw.meta.name,
author: raw.meta.author,
dark: raw.meta.dark,
};
Ok(TcaTheme {
meta,
ansi,
semantic,
ui,
base24,
})
}
}
impl PartialEq for TcaTheme {
fn eq(&self, other: &Self) -> bool {
self.name_slug() == other.name_slug()
}
}
impl Eq for TcaTheme {}
impl PartialOrd for TcaTheme {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for TcaTheme {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.name_slug().cmp(&other.name_slug())
}
}
#[derive(Debug, Clone, Default)]
pub struct TcaThemeBuilder {
meta: Meta,
ansi: Ansi,
semantic: Semantic,
ui: Ui,
}
impl TcaThemeBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn meta(mut self, meta: Meta) -> Self {
self.meta = meta;
self
}
pub fn ansi(mut self, ansi: Ansi) -> Self {
self.ansi = ansi;
self
}
pub fn semantic(mut self, semantic: Semantic) -> Self {
self.semantic = semantic;
self
}
pub fn ui(mut self, ui: Ui) -> Self {
self.ui = ui;
self
}
pub fn build(self) -> TcaTheme {
TcaTheme {
meta: self.meta,
ansi: self.ansi,
semantic: self.semantic,
ui: self.ui,
base24: [Color::Reset; 24],
}
}
}
#[cfg(feature = "fs")]
fn hex_to_color(hex: &str) -> Option<Color> {
let (r, g, b) = tca_types::hex_to_rgb(hex).ok()?;
Some(Color::Rgb(r, g, b))
}
#[cfg(feature = "fs")]
fn parse_ansi(raw: &tca_types::Ansi) -> Result<Ansi> {
let p = |hex: &str| -> Result<Color> {
hex_to_color(hex).with_context(|| format!("Invalid hex color in ansi: {:?}", hex))
};
Ok(Ansi {
black: p(&raw.black)?,
red: p(&raw.red)?,
green: p(&raw.green)?,
yellow: p(&raw.yellow)?,
blue: p(&raw.blue)?,
magenta: p(&raw.magenta)?,
cyan: p(&raw.cyan)?,
white: p(&raw.white)?,
bright_black: p(&raw.bright_black)?,
bright_red: p(&raw.bright_red)?,
bright_green: p(&raw.bright_green)?,
bright_yellow: p(&raw.bright_yellow)?,
bright_blue: p(&raw.bright_blue)?,
bright_magenta: p(&raw.bright_magenta)?,
bright_cyan: p(&raw.bright_cyan)?,
bright_white: p(&raw.bright_white)?,
})
}
#[cfg(feature = "fs")]
pub struct TcaThemeCursor(tca_types::ThemeCursor<TcaTheme>);
#[cfg(feature = "fs")]
impl TcaThemeCursor {
pub fn new(themes: impl IntoIterator<Item = TcaTheme>) -> Self {
Self(tca_types::ThemeCursor::new(themes))
}
pub fn with_builtins() -> Self {
let mut themes: Vec<TcaTheme> = tca_types::BuiltinTheme::iter()
.filter_map(|b| TcaTheme::try_from(b.theme()).ok())
.collect();
themes.sort();
Self::new(themes)
}
pub fn with_user_themes() -> Self {
let mut themes: Vec<TcaTheme> = tca_types::all_user_themes()
.into_iter()
.filter_map(|t| TcaTheme::try_from(t).ok())
.collect();
themes.sort();
Self::new(themes)
}
pub fn with_all_themes() -> Self {
let mut themes: Vec<TcaTheme> = tca_types::all_themes()
.into_iter()
.filter_map(|t| TcaTheme::try_from(t).ok())
.collect();
themes.sort();
Self::new(themes)
}
pub fn peek(&self) -> Option<&TcaTheme> {
self.0.peek()
}
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> Option<&TcaTheme> {
self.0.next()
}
pub fn prev(&mut self) -> Option<&TcaTheme> {
self.0.prev()
}
pub fn themes(&self) -> &[TcaTheme] {
self.0.themes()
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn set_current(&mut self, name: &str) -> Option<&TcaTheme> {
let slug = heck::AsKebabCase(name).to_string();
let idx = self.0.themes().iter().position(|t| t.name_slug() == slug)?;
self.0.set_index(idx)
}
}