From 0f1ef566dc67651bc3beaa2d3b76170280992875 Mon Sep 17 00:00:00 2001 From: Adam Milner Date: Sat, 14 Mar 2026 00:30:42 -0700 Subject: [PATCH 1/3] Add built in themes. --- tca-loader/Cargo.toml | 3 + tca-loader/src/lib.rs | 81 ++++++++++------- tca-ratatui/Cargo.toml | 2 + tca-ratatui/examples/basic.rs | 18 ++-- tca-ratatui/examples/picker.rs | 37 ++++++-- tca-ratatui/src/lib.rs | 2 + tca-ratatui/src/theme.rs | 63 +++++++++---- tca-types/Cargo.toml | 3 +- tca-types/src/builtin.rs | 71 +++++++++++++++ tca-types/src/lib.rs | 3 + tca-types/src/themes/catppuccin-mocha.toml | 99 ++++++++++++++++++++ tca-types/src/themes/cyberpunk.toml | 78 ++++++++++++++++ tca-types/src/themes/dracula.toml | 86 ++++++++++++++++++ tca-types/src/themes/everforest-dark.toml | 95 +++++++++++++++++++ tca-types/src/themes/gruvbox-dark.toml | 101 +++++++++++++++++++++ tca-types/src/themes/mono.toml | 77 ++++++++++++++++ tca-types/src/themes/nord-dark.toml | 89 ++++++++++++++++++ tca-types/src/themes/one-dark.toml | 94 +++++++++++++++++++ tca-types/src/themes/rose-pine.toml | 94 +++++++++++++++++++ tca-types/src/themes/solarized-light.toml | 87 ++++++++++++++++++ tca-types/src/themes/tokyo-night.toml | 97 ++++++++++++++++++++ 21 files changed, 1213 insertions(+), 67 deletions(-) create mode 100644 tca-types/src/builtin.rs create mode 100644 tca-types/src/themes/catppuccin-mocha.toml create mode 100644 tca-types/src/themes/cyberpunk.toml create mode 100644 tca-types/src/themes/dracula.toml create mode 100644 tca-types/src/themes/everforest-dark.toml create mode 100644 tca-types/src/themes/gruvbox-dark.toml create mode 100644 tca-types/src/themes/mono.toml create mode 100644 tca-types/src/themes/nord-dark.toml create mode 100644 tca-types/src/themes/one-dark.toml create mode 100644 tca-types/src/themes/rose-pine.toml create mode 100644 tca-types/src/themes/solarized-light.toml create mode 100644 tca-types/src/themes/tokyo-night.toml diff --git a/tca-loader/Cargo.toml b/tca-loader/Cargo.toml index 30efef2..5bf5f21 100644 --- a/tca-loader/Cargo.toml +++ b/tca-loader/Cargo.toml @@ -15,7 +15,10 @@ exclude = [".gitignore", "codebook.toml"] [dependencies] anyhow = { workspace = true } +confy = "2.0.0" +dark-light = "2.0.0" directories = { workspace = true } +etcetera = "0.11.0" serde = { workspace = true } tca-types = { workspace = true } toml = { workspace = true } diff --git a/tca-loader/src/lib.rs b/tca-loader/src/lib.rs index 9c7f680..6fab3da 100644 --- a/tca-loader/src/lib.rs +++ b/tca-loader/src/lib.rs @@ -6,41 +6,67 @@ #![warn(missing_docs)] use anyhow::{Context, Result}; -use directories::ProjectDirs; +use etcetera::{choose_app_strategy, AppStrategy, AppStrategyArgs}; +use serde::{Deserialize, Serialize}; use std::fs; use std::path::{Path, PathBuf}; -/// Resolve the TCA data directory path without creating it. -/// -/// Returns `$XDG_DATA_HOME/tca` on Linux/BSD, or the platform-equivalent on other OS. -fn resolve_data_dir() -> Result { - let project_dirs = - ProjectDirs::from("", "", "tca").context("Failed to determine project directories")?; - Ok(project_dirs.data_dir().to_path_buf()) +/// Configuration for TCA user preferences. +#[derive(Default, Debug, Serialize, Deserialize)] +pub struct TcaConfig { + /// The general default theme. Used if mode can't be detected or other options + /// aren't defined. + pub default_theme: Option, + /// Default dark mode theme. + pub default_dark_theme: Option, + /// Default light mode theme. + pub default_light_theme: Option, } -/// Get the TCA data directory path, creating it if it does not exist. -/// -/// Returns `$XDG_DATA_HOME/tca` on Linux/BSD, or the platform-equivalent on other OS. -pub fn get_data_dir() -> Result { - let data_dir = resolve_data_dir()?; - if !data_dir.exists() { - fs::create_dir_all(&data_dir) - .with_context(|| format!("Failed to create data directory: {:?}", data_dir))?; +impl TcaConfig { + /// Load the user's configuration preferences. + pub fn load() -> Self { + confy::load("tca", None).expect("Could not load TCA config.") + } + + /// Save the user's configurations preferences. + pub fn store(&self) { + confy::store("tca", None, self).expect("Could not save TCA config."); + } + + /// Get the best default theme, based on user preference and current system + /// color mode. + pub fn mode_aware_theme(&self) -> Option { + // Fallback order: + // Mode preference - if None or mode can't be determined then default + dark_light::detect().ok().and_then(|mode| match mode { + dark_light::Mode::Dark => self + .default_dark_theme + .clone() + .or(self.default_theme.clone()), + dark_light::Mode::Light => self + .default_light_theme + .clone() + .or(self.default_theme.clone()), + dark_light::Mode::Unspecified => self.default_theme.clone(), + }) } - Ok(data_dir) } /// Get the themes directory path, creating it if it does not exist. /// -/// Returns `$XDG_DATA_HOME/tca/themes` (or platform equivalent). +/// Returns `$XDG_DATA_HOME/tca-themes` (or platform equivalent). pub fn get_themes_dir() -> Result { - let themes_dir = resolve_data_dir()?.join("themes"); - if !themes_dir.exists() { - fs::create_dir_all(&themes_dir) - .with_context(|| format!("Failed to create themes directory: {:?}", themes_dir))?; - } - Ok(themes_dir) + let strategy = choose_app_strategy(AppStrategyArgs { + top_level_domain: "org".to_string(), + author: "TCA".to_string(), + app_name: "tca-themes".to_string(), + }) + .unwrap(); + let data_dir = strategy.data_dir(); + fs::create_dir_all(&data_dir)?; + + Ok(data_dir) } /// List all available theme files in the shared themes directory. @@ -186,13 +212,6 @@ pub fn load_all_from_theme_dir() -> Result> { mod tests { use super::*; - #[test] - fn test_get_data_dir() { - let dir = get_data_dir().unwrap(); - assert!(dir.exists()); - assert!(dir.to_string_lossy().contains("tca")); - } - #[test] fn test_get_themes_dir() { let dir = get_themes_dir().unwrap(); diff --git a/tca-ratatui/Cargo.toml b/tca-ratatui/Cargo.toml index 55bde0e..451c3fa 100644 --- a/tca-ratatui/Cargo.toml +++ b/tca-ratatui/Cargo.toml @@ -20,6 +20,8 @@ serde = { workspace = true } toml = { workspace = true } ratatui = { workspace = true } anyhow = { workspace = true } +strum = { version = "0.28.0", features = ["derive"] } +dark-light = "2.0.0" [dev-dependencies] crossterm = { workspace = true } diff --git a/tca-ratatui/examples/basic.rs b/tca-ratatui/examples/basic.rs index 00295e9..909a09f 100644 --- a/tca-ratatui/examples/basic.rs +++ b/tca-ratatui/examples/basic.rs @@ -1,21 +1,17 @@ use std::env; -use std::path::PathBuf; use ratatui::style::Style; use tca_ratatui::TcaTheme; fn main() -> Result<(), Box> { - let theme_path: Option = env::args_os().nth(1).map(PathBuf::from); + let arg = env::args().nth(1); + let theme_path: Option<&str> = arg.as_deref(); - let theme = match theme_path { - Some(theme_path) => { - println!("Loading TCA theme from: {:?}", theme_path); - TcaTheme::try_from(&theme_path)? - } - None => { - return Err("Usage: basic path/to/theme.toml".into()); - } - }; + if theme_path.is_none() { + return Err("Usage: basic path/to/theme.toml".into()); + } + println!("Loading TCA theme from: {:?}", theme_path); + let theme = TcaTheme::new(theme_path); println!("\nTheme: {}", theme.meta.name); if let Some(author) = theme.meta.author { diff --git a/tca-ratatui/examples/picker.rs b/tca-ratatui/examples/picker.rs index 97850b5..79bc0b2 100644 --- a/tca-ratatui/examples/picker.rs +++ b/tca-ratatui/examples/picker.rs @@ -2,7 +2,7 @@ use ratatui::{ crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind}, DefaultTerminal, Frame, }; -use tca_ratatui::{load_all_from_dir, load_all_from_theme_dir}; +use tca_ratatui::{load_all_builtin, load_all_from_dir, load_all_from_theme_dir}; use tca_ratatui::{ColorPicker, TcaTheme}; use std::{env, io}; @@ -64,11 +64,34 @@ impl App { } fn main() -> anyhow::Result<()> { - let themes_dir: Option = env::args().nth(1); + let args: Vec = env::args().collect(); - let mut themes = match &themes_dir { - Some(dir) => load_all_from_dir(dir)?, - None => load_all_from_theme_dir()?, + if args.iter().any(|a| a == "-h" || a == "--help") { + println!("Usage: picker [OPTIONS] [THEME_DIR]"); + println!(); + println!("Arguments:"); + println!(" [THEME_DIR] Load themes from a specific directory"); + println!(); + println!("Options:"); + println!(" --builtin Load built-in themes instead of user themes"); + println!(" -h, --help Print this help message"); + println!(); + println!("Keys:"); + println!(" ◀ / ▶ Previous / next theme"); + println!(" Q Quit"); + return Ok(()); + } + + let builtin_flag = args.iter().any(|a| a == "--builtin"); + let themes_dir = args.iter().skip(1).find(|a| !a.starts_with('-')).cloned(); + + let mut themes = if builtin_flag { + load_all_builtin() + } else { + match &themes_dir { + Some(dir) => load_all_from_dir(dir)?, + None => load_all_from_theme_dir()?, + } }; themes.sort_by_key(|t| t.meta.name.to_string()); @@ -78,8 +101,8 @@ fn main() -> anyhow::Result<()> { themes_dir.unwrap_or("user theme directory.".to_string()) ); eprintln!( - "Usage: {} [theme-directory]", - env::args().next().unwrap_or_default() + "Usage: {} [--builtin] [theme-directory]", + args.first().map(String::as_str).unwrap_or("picker") ); return Ok(()); } diff --git a/tca-ratatui/src/lib.rs b/tca-ratatui/src/lib.rs index e7a3f52..8d42a42 100644 --- a/tca-ratatui/src/lib.rs +++ b/tca-ratatui/src/lib.rs @@ -40,6 +40,8 @@ mod tests; pub use theme::{Ansi, Base16, ColorRamp, Meta, Palette, Semantic, TcaTheme, TcaThemeBuilder, Ui}; +pub use theme::load_all_builtin; + #[cfg(feature = "loader")] pub use theme::{load_all_from_dir, load_all_from_theme_dir}; diff --git a/tca-ratatui/src/theme.rs b/tca-ratatui/src/theme.rs index 6934260..841f14d 100644 --- a/tca-ratatui/src/theme.rs +++ b/tca-ratatui/src/theme.rs @@ -2,7 +2,8 @@ use anyhow::{Context, Result}; use ratatui::style::Color; use std::collections::HashMap; - +use strum::IntoEnumIterator; +use tca_types::BuiltinTheme; /// Theme metadata. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Meta { @@ -284,22 +285,41 @@ pub struct TcaTheme { pub ui: Ui, } -#[cfg(feature = "loader")] -/// Load a TcaTheme from a path. -impl TryFrom<&std::path::Path> for TcaTheme { - type Error = anyhow::Error; - fn try_from(path: &std::path::Path) -> Result { - let theme_str = std::fs::read_to_string(path)?; - TcaTheme::try_from(theme_str.as_str()) - } -} -#[cfg(feature = "loader")] -/// Load a TcaTheme from a path. -impl TryFrom<&std::path::PathBuf> for TcaTheme { - type Error = anyhow::Error; - fn try_from(path: &std::path::PathBuf) -> Result { - let theme_str = std::fs::read_to_string(path)?; - TcaTheme::try_from(theme_str.as_str()) +impl TcaTheme { + /// Creates a new theme, trying to match the passed name to a known + /// theme name or path and a reasonable default otherwise. + /// + /// The search fallback order is: + /// 1. Local theme files. + /// 2. Built in themes. + /// 3. User configured default theme. + /// 4. Built in default light/dark mode theme based on current mode. + #[cfg(feature = "loader")] + pub fn new(name: Option<&str>) -> Self { + // 1. Try loading by name/path from the themes directory + name.and_then(|n| tca_loader::load_theme_file(n).ok()) + .and_then(|s| TcaTheme::try_from(s.as_str()).ok()) + // 2. Try the named built-in theme + .or_else(|| { + name.and_then(|n| n.parse::().ok()) + .and_then(|b| TcaTheme::try_from(b.theme()).ok()) + }) + // 3. Try the global config default + // (e.g. ~/.config/tca/config.toml has `default_theme = "nord"`) + .or_else(|| { + tca_loader::TcaConfig::load() + .mode_aware_theme() + .and_then(|n| n.parse::().ok()) + .and_then(|b| TcaTheme::try_from(b.theme()).ok()) + }) + // 4. Hardcoded default — always succeeds + .unwrap_or_else(|| { + let builtin = match dark_light::detect().unwrap_or(dark_light::Mode::Dark) { + dark_light::Mode::Light => BuiltinTheme::default_light(), + _ => BuiltinTheme::default_dark(), + }; + TcaTheme::try_from(builtin.theme()).expect("hardcoded default must be valid") + }) } } @@ -374,6 +394,15 @@ impl TryFrom for TcaTheme { } } +/// Get a Vec of all built-in themes. +pub fn load_all_builtin() -> Vec { + BuiltinTheme::iter() + .map(|t| t.theme()) + .map(TcaTheme::try_from) + .filter_map(Result::ok) + .collect() +} + #[cfg(feature = "loader")] /// Loads and resolves all themes in a given directory. /// diff --git a/tca-types/Cargo.toml b/tca-types/Cargo.toml index cbbb0be..0ee6af4 100644 --- a/tca-types/Cargo.toml +++ b/tca-types/Cargo.toml @@ -16,7 +16,8 @@ exclude = [".gitignore", "codebook.toml"] [dependencies] anyhow = { workspace = true, optional = true } serde = { workspace = true } +strum = { version = "0.28.0", features = ["derive"] } thiserror = { workspace = true } -toml = { workspace = true, optional = true } +toml = { workspace = true } [features] diff --git a/tca-types/src/builtin.rs b/tca-types/src/builtin.rs new file mode 100644 index 0000000..d12761b --- /dev/null +++ b/tca-types/src/builtin.rs @@ -0,0 +1,71 @@ +#![allow(missing_docs)] +//! Built in hardcoded themes for tca-rust +use crate::Theme; +use strum::{EnumIter, EnumString, IntoStaticStr}; + +/// Enum of built in themes. + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, EnumIter, EnumString, IntoStaticStr)] +#[strum(serialize_all = "kebab-case")] +pub enum BuiltinTheme { + #[strum(serialize = "catppuccin-mocha", serialize = "catppuccin mocha")] + CatppuccinMocha, + Cyberpunk, + #[default] + Dracula, + #[strum(serialize = "everforest-dark", serialize = "everforest dark")] + EverforestDark, + #[strum(serialize = "gruvbox-dark", serialize = "gruvbox dark")] + GruvboxDark, + Mono, + Nord, + #[strum(serialize = "one-dark", serialize = "one dark")] + OneDark, + #[strum(serialize = "rose-pine", serialize = "rose pine")] + RosePine, + #[strum(serialize = "solarized light", serialize = "solarized-light")] + SolarizedLight, + #[strum(serialize = "tokyo-night", serialize = "tokyo night")] + TokyoNight, +} + +impl BuiltinTheme { + pub fn theme(&self) -> Theme { + let src = match self { + BuiltinTheme::CatppuccinMocha => include_str!("themes/catppuccin-mocha.toml"), + BuiltinTheme::Cyberpunk => include_str!("themes/cyberpunk.toml"), + BuiltinTheme::Dracula => include_str!("themes/dracula.toml"), + BuiltinTheme::EverforestDark => include_str!("themes/everforest-dark.toml"), + BuiltinTheme::GruvboxDark => include_str!("themes/gruvbox-dark.toml"), + BuiltinTheme::Mono => include_str!("themes/mono.toml"), + BuiltinTheme::Nord => include_str!("themes/nord-dark.toml"), + BuiltinTheme::OneDark => include_str!("themes/one-dark.toml"), + BuiltinTheme::RosePine => include_str!("themes/rose-pine.toml"), + BuiltinTheme::SolarizedLight => include_str!("themes/solarized-light.toml"), + BuiltinTheme::TokyoNight => include_str!("themes/tokyo-night.toml"), + }; + + toml::from_str(src).expect("Built in theme TOML is invalid.") + } + + pub fn default_dark() -> Self { + BuiltinTheme::default() + } + pub fn default_light() -> Self { + BuiltinTheme::SolarizedLight + } +} + +#[cfg(test)] +mod tests { + use super::*; + use strum::IntoEnumIterator; + + #[test] + fn all_builtin_themes_load_without_panic() { + for variant in BuiltinTheme::iter() { + let theme = variant.theme(); + assert!(!theme.meta.name.is_empty(), "{variant:?} has empty name"); + } + } +} diff --git a/tca-types/src/lib.rs b/tca-types/src/lib.rs index 2a5dd6e..27689fc 100644 --- a/tca-types/src/lib.rs +++ b/tca-types/src/lib.rs @@ -7,6 +7,9 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; +mod builtin; +pub use builtin::BuiltinTheme; + /// Errors that can occur when parsing a hex color string. #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] pub enum HexColorError { diff --git a/tca-types/src/themes/catppuccin-mocha.toml b/tca-types/src/themes/catppuccin-mocha.toml new file mode 100644 index 0000000..91d43d3 --- /dev/null +++ b/tca-types/src/themes/catppuccin-mocha.toml @@ -0,0 +1,99 @@ +[theme] +name = "Catppuccin Mocha" +slug = "catppuccin-mocha" +author = "Catppuccin Org / TCA Project" +version = "1.0.0" +description = "Soothing pastel dark theme. Based on Catppuccin Mocha by the Catppuccin Org." +dark = true + +[ansi] +black = "#45475a" +red = "#f38ba8" +green = "#a6e3a1" +yellow = "#f9e2af" +blue = "#89b4fa" +magenta = "#cba4f7" +cyan = "#94e2d5" +white = "#bac2de" + +bright_black = "#585b70" +bright_red = "#f38ba8" +bright_green = "#a6e3a1" +bright_yellow = "#f9e2af" +bright_blue = "#89b4fa" +bright_magenta = "#cba4f7" +bright_cyan = "#94e2d5" +bright_white = "#a6adc8" + +# Palette: crust→text (Catppuccin Mocha named tones) +# base: 0=#1e1e2e 1=#181825 2=#11111b (crust) +# surface: 0=#313244 1=#45475a 2=#585b70 +# overlay: 0=#6c7086 1=#7f849c 2=#9399b2 +# text: 0=#bac2de 1=#cdd6f4 2=#eff1f5 +[palette] +neutral = [ + "#11111b", + "#1e1e2e", + "#313244", + "#45475a", + "#585b70", + "#6c7086", + "#bac2de", + "#cdd6f4", +] +red = ["#7d1c2e", "#e6728a", "#f38ba8"] +green = ["#2a4734", "#89d8a0", "#a6e3a1"] +yellow = ["#5c4a1e", "#e5c890", "#f9e2af"] +blue = ["#1e3a5f", "#74aaef", "#89b4fa"] +mauve = ["#4d3066", "#b890f5", "#cba4f7"] +teal = ["#1b4a44", "#7fcfc3", "#94e2d5"] +peach = ["#5c2a0e", "#e08566", "#fab387"] +pink = ["#4d1f3c", "#ea7eb4", "#f5c2e7"] +sky = ["#1a445f", "#7fd4f7", "#89dceb"] + +[base16] +base00 = "palette.neutral.1" +base01 = "palette.neutral.0" +base02 = "palette.neutral.2" +base03 = "palette.neutral.3" +base04 = "palette.neutral.5" +base05 = "palette.neutral.7" +base06 = "palette.neutral.7" +base07 = "#eff1f5" +base08 = "palette.red.2" +base09 = "palette.peach.2" +base0A = "palette.yellow.2" +base0B = "palette.green.2" +base0C = "palette.teal.2" +base0D = "palette.blue.2" +base0E = "palette.mauve.2" +base0F = "palette.pink.2" + +[semantic] +error = "palette.red.2" +warning = "palette.peach.2" +info = "palette.sky.2" +success = "palette.green.2" +highlight = "palette.yellow.2" +link = "palette.blue.2" + +[ui.bg] +primary = "palette.neutral.1" +secondary = "palette.neutral.0" + +[ui.fg] +primary = "palette.neutral.7" +secondary = "palette.neutral.6" +muted = "palette.neutral.5" + +[ui.border] +primary = "palette.mauve.2" +muted = "palette.neutral.3" + +[ui.cursor] +primary = "palette.mauve.2" +muted = "palette.neutral.4" + +[ui.selection] +bg = "palette.neutral.3" +fg = "palette.neutral.7" diff --git a/tca-types/src/themes/cyberpunk.toml b/tca-types/src/themes/cyberpunk.toml new file mode 100644 index 0000000..9391caa --- /dev/null +++ b/tca-types/src/themes/cyberpunk.toml @@ -0,0 +1,78 @@ +[theme] +name = "Cyberpunk" +slug = "cyberpunk" +version = "1.0.0" +dark = true + +[ansi] +black = "#0d0221" +red = "#ff003c" +green = "#00ff64" +yellow = "#ffe600" +blue = "#00b4ff" +magenta = "#ff00ff" +cyan = "#00ffff" +white = "#f0f0f0" + +bright_black = "#28144f" +bright_red = "#ff3366" +bright_green = "#00ff96" +bright_yellow = "#ffee55" +bright_blue = "#33ccff" +bright_magenta = "#ff44ff" +bright_cyan = "#44ffff" +bright_white = "#ffffff" + +[palette] +neutral = ["ansi.black", "#140a2e", "#28144f", "#64648c", "ansi.white"] +neon_cyan = ["ansi.cyan", "ansi.bright_cyan"] +neon_pink = ["ansi.magenta", "ansi.bright_magenta"] +neon_green = ["ansi.green", "ansi.bright_green"] +neon_blue = ["ansi.blue", "ansi.bright_blue"] + +[base16] +base00 = "ansi.black" +base01 = "palette.neutral.1" +base02 = "palette.neutral.2" +base03 = "palette.neutral.3" +base04 = "ansi.cyan" +base05 = "ansi.white" +base06 = "ansi.bright_white" +base07 = "ansi.bright_cyan" +base08 = "ansi.red" +base09 = "ansi.yellow" +base0A = "ansi.bright_yellow" +base0B = "ansi.green" +base0C = "ansi.cyan" +base0D = "ansi.blue" +base0E = "ansi.magenta" +base0F = "ansi.bright_magenta" + +[semantic] +error = "ansi.red" +warning = "ansi.yellow" +success = "ansi.green" +info = "ansi.cyan" +highlight = "ansi.magenta" +link = "ansi.blue" + +[ui.bg] +primary = "palette.neutral.0" +secondary = "palette.neutral.1" + +[ui.fg] +primary = "ansi.white" +secondary = "ansi.cyan" +muted = "palette.neutral.3" + +[ui.border] +primary = "ansi.cyan" +muted = "palette.neutral.2" + +[ui.cursor] +primary = "ansi.magenta" +muted = "ansi.cyan" + +[ui.selection] +bg = "palette.neutral.2" +fg = "ansi.cyan" diff --git a/tca-types/src/themes/dracula.toml b/tca-types/src/themes/dracula.toml new file mode 100644 index 0000000..a513904 --- /dev/null +++ b/tca-types/src/themes/dracula.toml @@ -0,0 +1,86 @@ +[theme] +name = "Dracula" +slug = "dracula" +author = "Zeno Rocha / draculatheme.com" +version = "1.0.0" +description = "Based on https://draculatheme.com/" +dark = true + +[ansi] +black = "#21222C" +red = "#ff5555" +green = "#50fa7b" +yellow = "#f1fa8c" +blue = "#bd93f9" +magenta = "#ff79c6" +cyan = "#8be9fd" +white = "#f8f8f2" + +bright_black = "#6272a4" +bright_red = "#ff79c6" +bright_green = "#69ff94" +bright_yellow = "#ffffa5" +bright_blue = "#d6acff" +bright_magenta = "#ff92df" +bright_cyan = "#a4ffff" +bright_white = "#ffffff" + +[palette] +neutral = [ + "ansi.black", + "#282a36", + "#44475a", + "ansi.bright_black", + "ansi.white", +] +red = ["ansi.red", "#ff6e6e", "ansi.bright_red"] +purple = ["#bd93f9"] +orange = ["#ffb86c"] + + +[base16] +base00 = "ansi.black" +base01 = "ansi.red" +base02 = "ansi.green" +base03 = "ansi.yellow" +base04 = "ansi.blue" +base05 = "ansi.magenta" +base06 = "ansi.cyan" +base07 = "ansi.bright_black" +base08 = "ansi.bright_black" +base09 = "ansi.bright_red" +base0A = "ansi.bright_green" +base0B = "ansi.bright_yellow" +base0C = "ansi.bright_blue" +base0D = "ansi.bright_magenta" +base0E = "ansi.bright_cyan" +base0F = "ansi.bright_black" + +[semantic] +error = "ansi.red" +warning = "palette.orange.0" +success = "ansi.green" +info = "ansi.cyan" +highlight = "ansi.yellow" +link = "palette.purple.0" + +[ui.bg] +primary = "palette.neutral.0" +secondary = "palette.neutral.1" + +[ui.fg] +primary = "palette.neutral.4" +secondary = "palette.neutral.3" +muted = "palette.neutral.2" + +[ui.border] +primary = "palette.purple.0" +muted = "palette.neutral.1" + +[ui.cursor] +primary = "palette.neutral.3" +muted = "palette.neutral.2" + +[ui.selection] +bg = "palette.neutral.1" +fg = "palette.neutral.3" diff --git a/tca-types/src/themes/everforest-dark.toml b/tca-types/src/themes/everforest-dark.toml new file mode 100644 index 0000000..f819889 --- /dev/null +++ b/tca-types/src/themes/everforest-dark.toml @@ -0,0 +1,95 @@ +[theme] +name = "Everforest Dark" +slug = "everforest-dark" +author = "sainnhe / TCA Project" +version = "1.0.0" +description = "Comfortable green-tinted dark theme with warm, natural tones. Based on Everforest by sainnhe." +dark = true + +[ansi] +black = "#2b3339" +red = "#e67e80" +green = "#a7c080" +yellow = "#dbbc7f" +blue = "#7fbbb3" +magenta = "#d699b6" +cyan = "#83c092" +white = "#d3c6aa" + +bright_black = "#859289" +bright_red = "#e67e80" +bright_green = "#a7c080" +bright_yellow = "#dbbc7f" +bright_blue = "#7fbbb3" +bright_magenta = "#d699b6" +bright_cyan = "#83c092" +bright_white = "#e9e8d2" + +# Palette: darkest→lightest +# neutral: 0=#2b3339 1=#323c41 2=#3a464c 3=#434f55 4=#4a555b 5=#859289 6=#9da9a0 7=#d3c6aa 8=#e9e8d2 +[palette] +neutral = [ + "#2b3339", + "#323c41", + "#3a464c", + "#434f55", + "#4a555b", + "#859289", + "#9da9a0", + "#d3c6aa", + "#e9e8d2", +] +red = ["#5c1a1a", "#c2504f", "#e67e80"] +green = ["#2a4020", "#6e9a56", "#a7c080"] +yellow = ["#5c4010", "#b0922a", "#dbbc7f"] +blue = ["#1e3f45", "#4d8c8a", "#7fbbb3"] +purple = ["#3d1f45", "#a05c8a", "#d699b6"] +aqua = ["#1f3d2a", "#4e8a65", "#83c092"] +orange = ["#5c2e00", "#c47214", "#e69875"] + +[base16] +base00 = "palette.neutral.0" +base01 = "palette.neutral.1" +base02 = "palette.neutral.2" +base03 = "palette.neutral.3" +base04 = "palette.neutral.5" +base05 = "palette.neutral.7" +base06 = "palette.neutral.8" +base07 = "#e9e8d2" +base08 = "palette.red.2" +base09 = "palette.orange.2" +base0A = "palette.yellow.2" +base0B = "palette.green.2" +base0C = "palette.aqua.2" +base0D = "palette.blue.2" +base0E = "palette.purple.2" +base0F = "palette.red.1" + +[semantic] +error = "palette.red.2" +warning = "palette.orange.2" +info = "palette.blue.2" +success = "palette.green.2" +highlight = "palette.yellow.2" +link = "palette.blue.2" + +[ui.bg] +primary = "palette.neutral.0" +secondary = "palette.neutral.1" + +[ui.fg] +primary = "palette.neutral.8" +secondary = "palette.neutral.6" +muted = "palette.neutral.5" + +[ui.border] +primary = "palette.green.1" +muted = "palette.neutral.3" + +[ui.cursor] +primary = "palette.green.2" +muted = "palette.neutral.5" + +[ui.selection] +bg = "palette.neutral.2" +fg = "palette.neutral.7" diff --git a/tca-types/src/themes/gruvbox-dark.toml b/tca-types/src/themes/gruvbox-dark.toml new file mode 100644 index 0000000..28b59a2 --- /dev/null +++ b/tca-types/src/themes/gruvbox-dark.toml @@ -0,0 +1,101 @@ +[theme] +name = "Gruvbox Dark" +slug = "gruvbox-dark" +author = "morhetz / TCA Project" +version = "1.0.0" +description = "Retro groove dark theme with warm, earthy tones. Based on gruvbox by morhetz." +dark = true + +[ansi] +black = "#282828" +red = "#cc241d" +green = "#98971a" +yellow = "#d79921" +blue = "#458588" +magenta = "#b16286" +cyan = "#689d6a" +white = "#a89984" + +bright_black = "#928374" +bright_red = "#fb4934" +bright_green = "#b8bb26" +bright_yellow = "#fabd2f" +bright_blue = "#83a598" +bright_magenta = "#d3869b" +bright_cyan = "#8ec07c" +bright_white = "#ebdbb2" + +# Palette: darkest→lightest +# neutral: 0=#282828 1=#3c3836 2=#504945 3=#665c54 4=#a89984 5=#bdae93 6=#d5c4a1 7=#ebdbb2 8=#fbf1c7 +# red: 0=#9d0006 1=#cc241d 2=#fb4934 +# green: 0=#427b58 1=#98971a 2=#b8bb26 +# yellow: 0=#b57614 1=#d79921 2=#fabd2f +# blue: 0=#076678 1=#458588 2=#83a598 +# purple: 0=#8f3f71 1=#b16286 2=#d3869b +# aqua: 0=#427b58 1=#689d6a 2=#8ec07c +# orange: 0=#af3a03 1=#d65d0e 2=#fe8019 +[palette] +neutral = [ + "#282828", + "#3c3836", + "#504945", + "#665c54", + "#a89984", + "#bdae93", + "#d5c4a1", + "#ebdbb2", +] +red = ["#9d0006", "#cc241d", "#fb4934"] +green = ["#427b58", "#98971a", "#b8bb26"] +yellow = ["#b57614", "#d79921", "#fabd2f"] +blue = ["#076678", "#458588", "#83a598"] +purple = ["#8f3f71", "#b16286", "#d3869b"] +aqua = ["#427b58", "#689d6a", "#8ec07c"] +orange = ["#af3a03", "#d65d0e", "#fe8019"] + +[base16] +base00 = "palette.neutral.0" +base01 = "palette.neutral.1" +base02 = "palette.neutral.2" +base03 = "palette.neutral.3" +base04 = "palette.neutral.4" +base05 = "palette.neutral.7" +base06 = "ansi.bright_white" +base07 = "#fbf1c7" +base08 = "palette.red.1" +base09 = "palette.orange.1" +base0A = "palette.yellow.1" +base0B = "palette.green.1" +base0C = "palette.aqua.1" +base0D = "palette.blue.1" +base0E = "palette.purple.1" +base0F = "palette.red.0" + +[semantic] +error = "palette.red.2" +warning = "palette.yellow.2" +info = "palette.blue.2" +success = "palette.green.2" +highlight = "palette.yellow.1" +link = "palette.blue.2" + +[ui.bg] +primary = "palette.neutral.0" +secondary = "palette.neutral.1" + +[ui.fg] +primary = "ansi.bright_white" +secondary = "palette.neutral.7" +muted = "palette.neutral.4" + +[ui.border] +primary = "palette.neutral.3" +muted = "palette.neutral.2" + +[ui.cursor] +primary = "palette.yellow.2" +muted = "palette.neutral.4" + +[ui.selection] +bg = "palette.neutral.2" +fg = "ansi.bright_white" diff --git a/tca-types/src/themes/mono.toml b/tca-types/src/themes/mono.toml new file mode 100644 index 0000000..1ad0ea6 --- /dev/null +++ b/tca-types/src/themes/mono.toml @@ -0,0 +1,77 @@ +[theme] +name = "Mono" +slug = "mono" +author = "TCA Project" +version = "1.0.0" +description = "A basic monochromatic theme." +dark = true + + +[ansi] +black = "#0a0a0a" +red = "#606060" +green = "#d0d0d0" +yellow = "#d0d0d0" +blue = "#606060" +magenta = "#606060" +cyan = "#d0d0d0" +white = "#f5f5f5" +bright_black = "#2a2a2a" +bright_red = "#606060" +bright_green = "#d0d0d0" +bright_yellow = "#f5f5f5" +bright_blue = "#d0d0d0" +bright_magenta = "#606060" +bright_cyan = "#f5f5f5" +bright_white = "#f5f5f5" + +# Palette: 0=#0a0a0a 1=#2a2a2a 2=#606060 3=#d0d0d0 4=#f5f5f5 +[palette] +neutral = ["#0a0a0a", "#2a2a2a", "#606060", "#d0d0d0", "#f5f5f5"] + +[base16] +base00 = "palette.neutral.0" +base01 = "palette.neutral.1" +base02 = "palette.neutral.1" +base03 = "palette.neutral.2" +base04 = "palette.neutral.2" +base05 = "palette.neutral.3" +base06 = "palette.neutral.3" +base07 = "palette.neutral.4" +base08 = "palette.neutral.2" +base09 = "palette.neutral.2" +base0A = "palette.neutral.3" +base0B = "palette.neutral.3" +base0C = "palette.neutral.3" +base0D = "palette.neutral.2" +base0E = "palette.neutral.2" +base0F = "palette.neutral.2" + +[semantic] +error = "palette.neutral.2" +warning = "palette.neutral.2" +success = "palette.neutral.3" +info = "palette.neutral.3" +highlight = "palette.neutral.4" +link = "palette.neutral.3" + +[ui.bg] +primary = "palette.neutral.0" +secondary = "palette.neutral.1" + +[ui.fg] +primary = "palette.neutral.4" +secondary = "palette.neutral.3" +muted = "palette.neutral.2" + +[ui.border] +primary = "palette.neutral.2" +muted = "palette.neutral.1" + +[ui.cursor] +primary = "palette.neutral.3" +muted = "palette.neutral.2" + +[ui.selection] +bg = "palette.neutral.1" +fg = "palette.neutral.4" diff --git a/tca-types/src/themes/nord-dark.toml b/tca-types/src/themes/nord-dark.toml new file mode 100644 index 0000000..e7556ec --- /dev/null +++ b/tca-types/src/themes/nord-dark.toml @@ -0,0 +1,89 @@ +[theme] +name = "Nord Dark" +slug = "nord-dark" +author = "Arctic Ice Studio / TCA Project" +version = "1.0.0" +description = "An arctic, north-bluish color palette. Based on the Nord theme by Arctic Ice Studio." +dark = true + + +[ansi] +black = "#2e3440" +red = "#bf616a" +green = "#a3be8c" +yellow = "#ebcb8b" +blue = "#5e81ac" +magenta = "#b48ead" +cyan = "#8fbcbb" +white = "#d8dee9" + +bright_black = "#434c5e" +bright_red = "#bf616a" +bright_green = "#a3be8c" +bright_yellow = "#ebcb8b" +bright_blue = "#81a1c1" +bright_magenta = "#b48ead" +bright_cyan = "#88c0d0" +bright_white = "#eceff4" + +# Palette: all ramps are 0-indexed arrays, darkest→lightest +# neutral: 0=#2e3440 1=#3b4252 2=#434c5e 3=#d8dee9 4=#eceff4 +# frost: 0=#5e81ac 1=#81a1c1 2=#88c0d0 3=#8fbcbb +# single-entry ramps: 0 = only color +[palette] +neutral = [ + "ansi.black", + "#3b4252", + "ansi.bright_black", + "ansi.white", + "ansi.bright_white", +] +frost = ["#5e81ac", "#81a1c1", "#88c0d0", "#8fbcbb"] +orange = ["#d08770"] + +[base16] +base00 = "palette.neutral.0" +base01 = "palette.neutral.1" +base02 = "palette.neutral.2" +base03 = "palette.neutral.2" +base04 = "palette.neutral.3" +base05 = "palette.neutral.3" +base06 = "palette.neutral.4" +base07 = "palette.neutral.4" +base08 = "ansi.red" +base09 = "palette.orange.0" +base0A = "ansi.yellow" +base0B = "ansi.green" +base0C = "palette.frost.3" +base0D = "palette.frost.0" +base0E = "ansi.magenta" +base0F = "palette.orange.0" + +[semantic] +error = "ansi.red" +warning = "ansi.yellow" +success = "ansi.green" +info = "palette.frost.1" +highlight = "ansi.magenta" +link = "palette.frost.2" + +[ui.bg] +primary = "palette.neutral.0" +secondary = "palette.neutral.1" + +[ui.fg] +primary = "palette.neutral.4" +secondary = "palette.neutral.3" +muted = "palette.neutral.2" + +[ui.border] +primary = "palette.frost.0" +muted = "palette.neutral.2" + +[ui.cursor] +primary = "palette.neutral.3" +muted = "palette.neutral.2" + +[ui.selection] +bg = "palette.neutral.2" +fg = "palette.neutral.4" diff --git a/tca-types/src/themes/one-dark.toml b/tca-types/src/themes/one-dark.toml new file mode 100644 index 0000000..cbf5782 --- /dev/null +++ b/tca-types/src/themes/one-dark.toml @@ -0,0 +1,94 @@ +[theme] +name = "One Dark" +slug = "one-dark" +author = "Atom / TCA Project" +version = "1.0.0" +description = "Dark theme inspired by Atom's One Dark UI. Clean syntax colors on a comfortable dark background." +dark = true + +[ansi] +black = "#282c34" +red = "#e06c75" +green = "#98c379" +yellow = "#e5c07b" +blue = "#61afef" +magenta = "#c678dd" +cyan = "#56b6c2" +white = "#abb2bf" + +bright_black = "#5c6370" +bright_red = "#e06c75" +bright_green = "#98c379" +bright_yellow = "#e5c07b" +bright_blue = "#61afef" +bright_magenta = "#c678dd" +bright_cyan = "#56b6c2" +bright_white = "#ffffff" + +# Palette: darkest→lightest +# neutral: 0=#21252b 1=#282c34 2=#2c313a 3=#3e4451 4=#4b5263 5=#5c6370 6=#abb2bf 7=#c8ccd4 +[palette] +neutral = [ + "#21252b", + "#282c34", + "#2c313a", + "#3e4451", + "#4b5263", + "#5c6370", + "#abb2bf", + "#c8ccd4", +] +red = ["#6b1f25", "#be5046", "#e06c75"] +green = ["#2d4a24", "#6aaf50", "#98c379"] +yellow = ["#5c4510", "#caa234", "#e5c07b"] +blue = ["#1a3860", "#3d8ec9", "#61afef"] +purple = ["#3d1f5c", "#9e4ec7", "#c678dd"] +cyan = ["#1a3d42", "#3a8c96", "#56b6c2"] +orange = ["#5c2e00", "#c47214", "#d19a66"] + +[base16] +base00 = "palette.neutral.0" +base01 = "palette.neutral.1" +base02 = "palette.neutral.2" +base03 = "palette.neutral.3" +base04 = "palette.neutral.5" +base05 = "palette.neutral.6" +base06 = "palette.neutral.7" +base07 = "#ffffff" +base08 = "palette.red.2" +base09 = "palette.orange.2" +base0A = "palette.yellow.2" +base0B = "palette.green.2" +base0C = "palette.cyan.2" +base0D = "palette.blue.2" +base0E = "palette.purple.2" +base0F = "palette.red.1" + +[semantic] +error = "palette.red.2" +warning = "palette.orange.2" +info = "palette.blue.2" +success = "palette.green.2" +highlight = "palette.yellow.2" +link = "palette.blue.2" + +[ui.bg] +primary = "palette.neutral.1" +secondary = "palette.neutral.2" + +[ui.fg] +primary = "palette.neutral.6" +secondary = "palette.neutral.5" +muted = "ansi.bright_black" + +[ui.border] +primary = "palette.neutral.4" +muted = "palette.neutral.3" + +[ui.cursor] +primary = "palette.blue.2" +muted = "palette.neutral.5" + +[ui.selection] +bg = "palette.neutral.3" +fg = "palette.neutral.6" diff --git a/tca-types/src/themes/rose-pine.toml b/tca-types/src/themes/rose-pine.toml new file mode 100644 index 0000000..17580a2 --- /dev/null +++ b/tca-types/src/themes/rose-pine.toml @@ -0,0 +1,94 @@ +[theme] +name = "Rosé Pine" +slug = "rose-pine" +author = "Rosé Pine / TCA Project" +version = "1.0.0" +description = "All natural pine, faux fur and a bit of soho vibes for the classy minimalist. Based on Rosé Pine by Rosé Pine." +dark = true + +[ansi] +black = "#191724" +red = "#eb6f92" +green = "#31748f" +yellow = "#f6c177" +blue = "#9ccfd8" +magenta = "#c4a7e7" +cyan = "#ebbcba" +white = "#e0def4" + +bright_black = "#6e6a86" +bright_red = "#eb6f92" +bright_green = "#31748f" +bright_yellow = "#f6c177" +bright_blue = "#9ccfd8" +bright_magenta = "#c4a7e7" +bright_cyan = "#ebbcba" +bright_white = "#e0def4" + +# Palette: base→text (Rosé Pine named tones) +# base: 0=#191724 1=#1f1d2e 2=#26233a +# surface: 0=#403d52 1=#524f67 2=#6e6a86 +# text: 0=#908caa 1=#e0def4 +[palette] +neutral = [ + "#191724", + "#1f1d2e", + "#26233a", + "#403d52", + "#524f67", + "#6e6a86", + "#908caa", + "#e0def4", +] +rose = ["#5c2035", "#c4506b", "#eb6f92"] +pine = ["#0f3040", "#20607a", "#31748f"] +gold = ["#5c3d00", "#c29540", "#f6c177"] +foam = ["#1a4a50", "#5da5af", "#9ccfd8"] +iris = ["#3a2460", "#8d6dc4", "#c4a7e7"] + +[base16] +base00 = "palette.neutral.1" +base01 = "palette.neutral.2" +base02 = "palette.neutral.3" +base03 = "palette.neutral.4" +base04 = "palette.neutral.6" +base05 = "palette.neutral.7" +base06 = "palette.neutral.7" +base07 = "#faf4ed" +base08 = "palette.rose.2" +base09 = "palette.gold.2" +base0A = "palette.gold.2" +base0B = "palette.pine.2" +base0C = "palette.foam.2" +base0D = "palette.iris.2" +base0E = "palette.iris.2" +base0F = "ansi.bright_cyan" + +[semantic] +error = "palette.rose.2" +warning = "palette.gold.2" +info = "palette.foam.2" +success = "palette.pine.2" +highlight = "palette.gold.1" +link = "palette.foam.2" + +[ui.bg] +primary = "palette.neutral.1" +secondary = "palette.neutral.2" + +[ui.fg] +primary = "palette.neutral.7" +secondary = "palette.neutral.6" +muted = "palette.neutral.5" + +[ui.border] +primary = "palette.iris.1" +muted = "palette.neutral.3" + +[ui.cursor] +primary = "palette.rose.2" +muted = "palette.neutral.5" + +[ui.selection] +bg = "palette.neutral.3" +fg = "palette.neutral.7" diff --git a/tca-types/src/themes/solarized-light.toml b/tca-types/src/themes/solarized-light.toml new file mode 100644 index 0000000..967a0e0 --- /dev/null +++ b/tca-types/src/themes/solarized-light.toml @@ -0,0 +1,87 @@ +[theme] +name = "Solarized Light" +slug = "solarized-light" +author = "TCA Project" +version = "1.0.0" +dark = false + + +[ansi] +black = "#002b36" +red = "#dc322f" +green = "#859900" +yellow = "#b58900" +blue = "#268bd2" +magenta = "#d33682" +cyan = "#2aa198" +white = "#eee8d5" + +bright_black = "#586e75" +bright_red = "#cb4b16" +bright_green = "#859900" +bright_yellow = "#b58900" +bright_blue = "#268bd2" +bright_magenta = "#6c71c4" +bright_cyan = "#2aa198" +bright_white = "#fdf6e3" + +# Palette: all ramps are 0-indexed arrays, darkest→lightest +# neutral: 0=#002b36 1=#586e75 2=#93a1a1 3=#eee8d5 4=#fdf6e3 +# single-entry ramps: 0 = only color +[palette] +neutral = [ + "ansi.black", + "ansi.bright_black", + "#93a1a1", + "ansi.white", + "ansi.bright_white", +] +orange = ["#cb4b16"] +violet = ["#6c71c4"] + +[base16] +base00 = "palette.neutral.4" +base01 = "palette.neutral.3" +base02 = "palette.neutral.3" +base03 = "palette.neutral.2" +base04 = "palette.neutral.2" +base05 = "palette.neutral.1" +base06 = "palette.neutral.1" +base07 = "palette.neutral.0" +base08 = "ansi.red" +base09 = "palette.orange.0" +base0A = "ansi.yellow" +base0B = "ansi.green" +base0C = "ansi.cyan" +base0D = "ansi.blue" +base0E = "ansi.magenta" +base0F = "palette.violet.0" + +[semantic] +error = "ansi.red" +warning = "ansi.yellow" +success = "ansi.green" +info = "ansi.blue" +highlight = "ansi.yellow" +link = "ansi.blue" + +[ui.bg] +primary = "palette.neutral.4" +secondary = "palette.neutral.3" + +[ui.fg] +primary = "palette.neutral.0" +secondary = "palette.neutral.1" +muted = "palette.neutral.2" + +[ui.border] +primary = "ansi.blue" +muted = "palette.neutral.2" + +[ui.cursor] +primary = "palette.neutral.0" +muted = "palette.neutral.2" + +[ui.selection] +bg = "palette.neutral.3" +fg = "palette.neutral.0" diff --git a/tca-types/src/themes/tokyo-night.toml b/tca-types/src/themes/tokyo-night.toml new file mode 100644 index 0000000..1f7b93a --- /dev/null +++ b/tca-types/src/themes/tokyo-night.toml @@ -0,0 +1,97 @@ +[theme] +name = "Tokyo Night" +slug = "tokyo-night" +author = "enkia / TCA Project" +version = "1.0.0" +description = "A clean, dark theme inspired by the city lights of Tokyo at night. Based on Tokyo Night by enkia." +dark = true + +[ansi] +black = "#15161e" +red = "#f7768e" +green = "#9ece6a" +yellow = "#e0af68" +blue = "#7aa2f7" +magenta = "#bb9af7" +cyan = "#7dcfff" +white = "#a9b1d6" + +bright_black = "#414868" +bright_red = "#f7768e" +bright_green = "#9ece6a" +bright_yellow = "#e0af68" +bright_blue = "#7aa2f7" +bright_magenta = "#bb9af7" +bright_cyan = "#7dcfff" +bright_white = "#c0caf5" + +# Palette: darkest→lightest +# neutral: bg_dark→fg +# 0=#15161e 1=#1a1b26 2=#24283b 3=#292e42 4=#414868 5=#565f89 6=#a9b1d6 7=#c0caf5 +[palette] +neutral = [ + "#15161e", + "#1a1b26", + "#24283b", + "#292e42", + "#414868", + "#565f89", + "#a9b1d6", + "#c0caf5", +] +red = ["#6b1a2a", "#db4b4b", "#f7768e"] +green = ["#2d4a1e", "#41a146", "#9ece6a"] +yellow = ["#5c4400", "#c09230", "#e0af68"] +blue = ["#1a2a6b", "#3d59a1", "#7aa2f7"] +purple = ["#3d2370", "#7953c7", "#bb9af7"] +cyan = ["#144a60", "#0db9d7", "#7dcfff"] +orange = ["#5c2e00", "#c47214", "#ff9e64"] +teal = ["#17403d", "#1abc9c", "#73daca"] +magenta = ["#5c1a5c", "#9d4bb6", "#c586c0"] + +[base16] +base00 = "palette.neutral.1" +base01 = "palette.neutral.2" +base02 = "palette.neutral.3" +base03 = "palette.neutral.4" +base04 = "palette.neutral.5" +base05 = "palette.neutral.7" +base06 = "palette.neutral.7" +base07 = "#d5d6db" +base08 = "palette.red.2" +base09 = "palette.orange.2" +base0A = "palette.yellow.2" +base0B = "palette.green.2" +base0C = "palette.teal.2" +base0D = "palette.blue.2" +base0E = "palette.purple.2" +base0F = "palette.magenta.2" + +[semantic] +error = "palette.red.2" +warning = "palette.orange.2" +info = "palette.cyan.2" +success = "palette.green.2" +highlight = "palette.yellow.2" +link = "palette.blue.2" + +[ui.bg] +primary = "palette.neutral.1" +secondary = "palette.neutral.2" + +[ui.fg] +primary = "palette.neutral.7" +secondary = "palette.neutral.6" +muted = "palette.neutral.5" + +[ui.border] +primary = "palette.blue.1" +muted = "palette.neutral.3" + +[ui.cursor] +primary = "palette.blue.2" +muted = "palette.neutral.5" + +[ui.selection] +bg = "palette.neutral.3" +fg = "palette.neutral.7" From f81c717e4cc26c61f88ae7238fa88c6d7b9f71a3 Mon Sep 17 00:00:00 2001 From: Adam Milner Date: Sat, 14 Mar 2026 12:02:50 -0700 Subject: [PATCH 2/3] Fix tests. --- tca-loader/src/lib.rs | 2 +- tca-ratatui/src/lib.rs | 95 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 91 insertions(+), 6 deletions(-) diff --git a/tca-loader/src/lib.rs b/tca-loader/src/lib.rs index 6fab3da..c444991 100644 --- a/tca-loader/src/lib.rs +++ b/tca-loader/src/lib.rs @@ -216,7 +216,7 @@ mod tests { fn test_get_themes_dir() { let dir = get_themes_dir().unwrap(); assert!(dir.exists()); - assert!(dir.ends_with("themes")); + assert!(dir.ends_with("tca-themes")); } #[test] diff --git a/tca-ratatui/src/lib.rs b/tca-ratatui/src/lib.rs index 8d42a42..026ec91 100644 --- a/tca-ratatui/src/lib.rs +++ b/tca-ratatui/src/lib.rs @@ -9,9 +9,95 @@ //! use ratatui::style::Style; //! use std::path::Path; //! -//! # fn example() -> Result<(), Box> { -//! // Load a TCA theme from a file -//! let theme = TcaTheme::try_from(Path::new("theme.toml"))?; +//! fn example() { +//! // Load a TCA theme from a file or name, with reasonable fallback. +//! let theme = TcaTheme::new(Some("theme.toml")); +//! let theme = TcaTheme::new(Some("Tokyo Night")); +//! let theme = TcaTheme::new(Some("tokyo-night")); +//! let theme = TcaTheme::new(None); +//! +//! // Or from a TOML string. +//! let toml_str = r###" +//! [theme] +//! name = "Tokyo Night" +//! slug = "tokyo-night" +//! author = "enkia / TCA Project" +//! version = "1.0.0" +//! description = "A clean, dark theme inspired by the city lights of Tokyo at night. Based on Tokyo Night by enkia." +//! dark = true +//! +//! [ansi] +//! black = "#15161e" +//! red = "#f7768e" +//! green = "#9ece6a" +//! yellow = "#e0af68" +//! blue = "#7aa2f7" +//! magenta = "#bb9af7" +//! cyan = "#7dcfff" +//! white = "#a9b1d6" +//! bright_black = "#414868" +//! bright_red = "#f7768e" +//! bright_green = "#9ece6a" +//! bright_yellow = "#e0af68" +//! bright_blue = "#7aa2f7" +//! bright_magenta = "#bb9af7" +//! bright_cyan = "#7dcfff" +//! bright_white = "#c0caf5" +//! +//! # Palette: darkest→lightest +//! # neutral: bg_dark→fg +//! # 0=#15161e 1=#1a1b26 2=#24283b 3=#292e42 4=#414868 5=#565f89 6=#a9b1d6 7=#c0caf5 +//! [palette] +//! neutral = [ +//! "#15161e", +//! "#1a1b26", +//! "#24283b", +//! "#292e42", +//! "#414868", +//! "#565f89", +//! "#a9b1d6", +//! "#c0caf5", +//! ] +//! red = ["#6b1a2a", "#db4b4b", "#f7768e"] +//! green = ["#2d4a1e", "#41a146", "#9ece6a"] +//! yellow = ["#5c4400", "#c09230", "#e0af68"] +//! blue = ["#1a2a6b", "#3d59a1", "#7aa2f7"] +//! purple = ["#3d2370", "#7953c7", "#bb9af7"] +//! cyan = ["#144a60", "#0db9d7", "#7dcfff"] +//! orange = ["#5c2e00", "#c47214", "#ff9e64"] +//! teal = ["#17403d", "#1abc9c", "#73daca"] +//! magenta = ["#5c1a5c", "#9d4bb6", "#c586c0"] +//! +//! [semantic] +//! error = "palette.red.2" +//! warning = "palette.orange.2" +//! info = "palette.cyan.2" +//! success = "palette.green.2" +//! highlight = "palette.yellow.2" +//! link = "palette.blue.2" +//! +//! [ui.bg] +//! primary = "palette.neutral.1" +//! secondary = "palette.neutral.2" +//! +//! [ui.fg] +//! primary = "palette.neutral.7" +//! secondary = "palette.neutral.6" +//! muted = "palette.neutral.5" +//! +//! [ui.border] +//! primary = "palette.blue.1" +//! muted = "palette.neutral.3" +//! +//! [ui.cursor] +//! primary = "palette.blue.2" +//! muted = "palette.neutral.5" +//! +//! [ui.selection] +//! bg = "palette.neutral.3" +//! fg = "palette.neutral.7" +//! "###; +//! let theme = TcaTheme::try_from(toml_str).expect("Couldn't parse TOML"); //! //! // Use ANSI colors //! let error_style = Style::default().fg(theme.ansi.red); @@ -24,8 +110,7 @@ //! // Use UI colors //! let bg_style = Style::default().bg(theme.ui.bg_primary); //! let fg_style = Style::default().fg(theme.ui.fg_primary); -//! # Ok(()) -//! # } +//! } //! ``` #![warn(missing_docs)] From f6afd641a889fa7c0da43307c4afbe3903119794 Mon Sep 17 00:00:00 2001 From: Adam Milner Date: Sat, 14 Mar 2026 12:37:27 -0700 Subject: [PATCH 3/3] Clean up deps and simplify interface. --- Cargo.toml | 2 +- tca-loader/Cargo.toml | 4 +--- tca-loader/src/lib.rs | 42 ++++++++++++++++++++++++++-------- tca-ratatui/Cargo.toml | 2 +- tca-ratatui/examples/picker.rs | 29 +++++++++++++++++++---- tca-ratatui/src/lib.rs | 3 --- tca-ratatui/src/theme.rs | 34 ++++----------------------- 7 files changed, 65 insertions(+), 51 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2b3f867..13b8a3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,10 +25,10 @@ thiserror = "2.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" anyhow = "1.0" -directories = "5.0" colored = "2.1" jsonschema = "0.42" toml = "0.8" clap = { version = "4.5", features = ["derive"] } ratatui = "0.30" crossterm = "0.29" +terminal-colorsaurus = "1" diff --git a/tca-loader/Cargo.toml b/tca-loader/Cargo.toml index 5bf5f21..50db03c 100644 --- a/tca-loader/Cargo.toml +++ b/tca-loader/Cargo.toml @@ -15,9 +15,7 @@ exclude = [".gitignore", "codebook.toml"] [dependencies] anyhow = { workspace = true } -confy = "2.0.0" -dark-light = "2.0.0" -directories = { workspace = true } +terminal-colorsaurus = { workspace = true } etcetera = "0.11.0" serde = { workspace = true } tca-types = { workspace = true } diff --git a/tca-loader/src/lib.rs b/tca-loader/src/lib.rs index c444991..9ed763e 100644 --- a/tca-loader/src/lib.rs +++ b/tca-loader/src/lib.rs @@ -23,33 +23,57 @@ pub struct TcaConfig { pub default_light_theme: Option, } +/// Returns the path to the TCA config file (`tca.toml` in the app config dir). +fn config_file_path() -> Result { + let strategy = choose_app_strategy(AppStrategyArgs { + top_level_domain: "org".to_string(), + author: "TCA".to_string(), + app_name: "tca".to_string(), + })?; + Ok(strategy.config_dir().join("tca.toml")) +} + impl TcaConfig { /// Load the user's configuration preferences. + /// + /// Returns [`Default`] if the config file doesn't exist or cannot be parsed. pub fn load() -> Self { - confy::load("tca", None).expect("Could not load TCA config.") + let Ok(path) = config_file_path() else { + return Self::default(); + }; + let Ok(content) = fs::read_to_string(path) else { + return Self::default(); + }; + toml::from_str(&content).unwrap_or_default() } - /// Save the user's configurations preferences. + /// Save the user's configuration preferences. pub fn store(&self) { - confy::store("tca", None, self).expect("Could not save TCA config."); + let path = config_file_path().expect("Could not determine TCA config path."); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).expect("Could not create TCA config directory."); + } + let content = toml::to_string(self).expect("Could not serialize TCA config."); + fs::write(&path, content).expect("Could not save TCA config."); } - /// Get the best default theme, based on user preference and current system + /// Get the best default theme, based on user preference and current terminal /// color mode. pub fn mode_aware_theme(&self) -> Option { // Fallback order: // Mode preference - if None or mode can't be determined then default - dark_light::detect().ok().and_then(|mode| match mode { - dark_light::Mode::Dark => self + use terminal_colorsaurus::{theme_mode, QueryOptions, ThemeMode}; + match theme_mode(QueryOptions::default()).ok() { + Some(ThemeMode::Dark) => self .default_dark_theme .clone() .or(self.default_theme.clone()), - dark_light::Mode::Light => self + Some(ThemeMode::Light) => self .default_light_theme .clone() .or(self.default_theme.clone()), - dark_light::Mode::Unspecified => self.default_theme.clone(), - }) + None => self.default_theme.clone(), + } } } diff --git a/tca-ratatui/Cargo.toml b/tca-ratatui/Cargo.toml index 451c3fa..11a4fcd 100644 --- a/tca-ratatui/Cargo.toml +++ b/tca-ratatui/Cargo.toml @@ -21,7 +21,7 @@ toml = { workspace = true } ratatui = { workspace = true } anyhow = { workspace = true } strum = { version = "0.28.0", features = ["derive"] } -dark-light = "2.0.0" +terminal-colorsaurus = { workspace = true } [dev-dependencies] crossterm = { workspace = true } diff --git a/tca-ratatui/examples/picker.rs b/tca-ratatui/examples/picker.rs index 79bc0b2..90dee59 100644 --- a/tca-ratatui/examples/picker.rs +++ b/tca-ratatui/examples/picker.rs @@ -2,8 +2,7 @@ use ratatui::{ crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind}, DefaultTerminal, Frame, }; -use tca_ratatui::{load_all_builtin, load_all_from_dir, load_all_from_theme_dir}; -use tca_ratatui::{ColorPicker, TcaTheme}; +use tca_ratatui::{load_all_builtin, ColorPicker, TcaTheme}; use std::{env, io}; @@ -63,6 +62,28 @@ impl App { } } +fn load_from_dir(dir: &str) -> anyhow::Result> { + let paths: Vec<_> = std::fs::read_dir(dir)? + .filter_map(|e| e.ok()) + .filter(|e| e.path().extension().and_then(|x| x.to_str()) == Some("toml")) + .map(|e| e.path()) + .collect(); + Ok(paths + .iter() + .filter_map(|p| p.to_str()) + .map(|s| TcaTheme::new(Some(s))) + .collect()) +} + +fn load_from_theme_dir() -> anyhow::Result> { + let paths = tca_loader::list_themes()?; + Ok(paths + .iter() + .filter_map(|p| p.to_str()) + .map(|s| TcaTheme::new(Some(s))) + .collect()) +} + fn main() -> anyhow::Result<()> { let args: Vec = env::args().collect(); @@ -89,8 +110,8 @@ fn main() -> anyhow::Result<()> { load_all_builtin() } else { match &themes_dir { - Some(dir) => load_all_from_dir(dir)?, - None => load_all_from_theme_dir()?, + Some(dir) => load_from_dir(dir)?, + None => load_from_theme_dir()?, } }; themes.sort_by_key(|t| t.meta.name.to_string()); diff --git a/tca-ratatui/src/lib.rs b/tca-ratatui/src/lib.rs index 026ec91..d99d7ec 100644 --- a/tca-ratatui/src/lib.rs +++ b/tca-ratatui/src/lib.rs @@ -127,8 +127,5 @@ pub use theme::{Ansi, Base16, ColorRamp, Meta, Palette, Semantic, TcaTheme, TcaT pub use theme::load_all_builtin; -#[cfg(feature = "loader")] -pub use theme::{load_all_from_dir, load_all_from_theme_dir}; - #[cfg(feature = "widgets")] pub use widgets::ColorPicker; diff --git a/tca-ratatui/src/theme.rs b/tca-ratatui/src/theme.rs index 841f14d..5af0cb8 100644 --- a/tca-ratatui/src/theme.rs +++ b/tca-ratatui/src/theme.rs @@ -314,8 +314,9 @@ impl TcaTheme { }) // 4. Hardcoded default — always succeeds .unwrap_or_else(|| { - let builtin = match dark_light::detect().unwrap_or(dark_light::Mode::Dark) { - dark_light::Mode::Light => BuiltinTheme::default_light(), + use terminal_colorsaurus::{theme_mode, QueryOptions, ThemeMode}; + let builtin = match theme_mode(QueryOptions::default()).ok() { + Some(ThemeMode::Light) => BuiltinTheme::default_light(), _ => BuiltinTheme::default_dark(), }; TcaTheme::try_from(builtin.theme()).expect("hardcoded default must be valid") @@ -333,10 +334,7 @@ impl TryFrom<&str> for TcaTheme { } } -/// Resolves a raw [`tca_types::Theme`] into a [`TcaTheme`] with Ratatui colors. -/// -/// All color references that cannot be resolved silently fall back to defaults. -/// Except for Ansi colors, as they are a hard requirement. +#[doc(hidden)] impl TryFrom for TcaTheme { type Error = anyhow::Error; fn try_from(raw: tca_types::Theme) -> Result { @@ -403,30 +401,6 @@ pub fn load_all_builtin() -> Vec { .collect() } -#[cfg(feature = "loader")] -/// Loads and resolves all themes in a given directory. -/// -/// Themes that can't be parsed are skipped. -pub fn load_all_from_dir(dir: &str) -> anyhow::Result> { - let raw = tca_loader::load_all_from_dir(dir)?; - let themes = raw - .into_iter() - .map(TcaTheme::try_from) - .filter_map(Result::ok) - .collect(); - Ok(themes) -} - -#[cfg(feature = "loader")] -/// Loads and resolves all themes in the user theme directory. -/// -/// Themes that can't be parsed are skipped. -pub fn load_all_from_theme_dir() -> anyhow::Result> { - let dir = tca_loader::get_themes_dir()?; - let dir_str = dir.to_str().context("Data directory is not valid.")?; - load_all_from_dir(dir_str) -} - /// Builder for constructing a [`TcaTheme`] programmatically. /// /// All sections default to sensible fallback values so you only need to