aboutsummaryrefslogtreecommitdiffstats
path: root/crates/atuin-client/src/theme.rs
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-06-11 00:54:30 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-06-11 00:54:30 +0200
commit5c39e7cf284a1f6e9a1657f2deb44e359fc47eb8 (patch)
treec64baa8d5866c8e339eaf660dd3f94f30a3f7d8a /crates/atuin-client/src/theme.rs
parentchore: Somewhat simplify sync code (diff)
downloadatuin-5c39e7cf284a1f6e9a1657f2deb44e359fc47eb8.zip
chore: Move everything into one big crate
That helps remove duplicated code and rustc/cargo will now also show dead code correctly.
Diffstat (limited to 'crates/atuin-client/src/theme.rs')
-rw-r--r--crates/atuin-client/src/theme.rs831
1 files changed, 0 insertions, 831 deletions
diff --git a/crates/atuin-client/src/theme.rs b/crates/atuin-client/src/theme.rs
deleted file mode 100644
index a277ac13..00000000
--- a/crates/atuin-client/src/theme.rs
+++ /dev/null
@@ -1,831 +0,0 @@
-use config::{Config, File as ConfigFile, FileFormat};
-use log;
-use palette::named;
-use serde::{Deserialize, Serialize};
-use serde_json;
-use std::collections::HashMap;
-use std::error;
-use std::io::{Error, ErrorKind};
-use std::path::PathBuf;
-use std::sync::LazyLock;
-use strum_macros;
-
-static DEFAULT_MAX_DEPTH: u8 = 10;
-
-// Collection of settable "meanings" that can have colors set.
-// NOTE: You can add a new meaning here without breaking backwards compatibility but please:
-// - update the atuin/docs repository, which has a list of available meanings
-// - add a fallback in the MEANING_FALLBACKS below, so that themes which do not have it
-// get a sensible fallback (see Title as an example)
-#[derive(
- Serialize, Deserialize, Copy, Clone, Hash, Debug, Eq, PartialEq, strum_macros::Display,
-)]
-#[strum(serialize_all = "camel_case")]
-pub enum Meaning {
- AlertInfo,
- AlertWarn,
- AlertError,
- Annotation,
- Base,
- Guidance,
- Important,
- Title,
- Muted,
-}
-
-#[derive(Clone, Debug, Deserialize, Serialize)]
-pub struct ThemeConfig {
- // Definition of the theme
- pub theme: ThemeDefinitionConfigBlock,
-
- // Colors
- pub colors: HashMap<Meaning, String>,
-}
-
-#[derive(Clone, Debug, Deserialize, Serialize)]
-pub struct ThemeDefinitionConfigBlock {
- /// Name of theme ("default" for base)
- pub name: String,
-
- /// Whether any theme should be treated as a parent _if available_
- pub parent: Option<String>,
-}
-
-use crossterm::style::{Attribute, Attributes, Color, ContentStyle};
-
-// For now, a theme is loaded as a mapping of meanings to colors, but it may be desirable to
-// expand that in the future to general styles, so we populate a Meaning->ContentStyle hashmap.
-pub struct Theme {
- pub name: String,
- pub parent: Option<String>,
- pub styles: HashMap<Meaning, ContentStyle>,
-}
-
-// Themes have a number of convenience functions for the most commonly used meanings.
-// The general purpose `as_style` routine gives back a style, but for ease-of-use and to keep
-// theme-related boilerplate minimal, the convenience functions give a color.
-impl Theme {
- // This is the base "default" color, for general text
- pub fn get_base(&self) -> ContentStyle {
- self.styles[&Meaning::Base]
- }
-
- pub fn get_info(&self) -> ContentStyle {
- self.get_alert(log::Level::Info)
- }
-
- pub fn get_warning(&self) -> ContentStyle {
- self.get_alert(log::Level::Warn)
- }
-
- pub fn get_error(&self) -> ContentStyle {
- self.get_alert(log::Level::Error)
- }
-
- // The alert meanings may be chosen by the Level enum, rather than the methods above
- // or the full Meaning enum, to simplify programmatic selection of a log-level.
- pub fn get_alert(&self, severity: log::Level) -> ContentStyle {
- self.styles[ALERT_TYPES.get(&severity).unwrap()]
- }
-
- pub fn new(
- name: String,
- parent: Option<String>,
- styles: HashMap<Meaning, ContentStyle>,
- ) -> Theme {
- Theme {
- name,
- parent,
- styles,
- }
- }
-
- pub fn closest_meaning<'a>(&self, meaning: &'a Meaning) -> &'a Meaning {
- if self.styles.contains_key(meaning) {
- meaning
- } else if MEANING_FALLBACKS.contains_key(meaning) {
- self.closest_meaning(&MEANING_FALLBACKS[meaning])
- } else {
- &Meaning::Base
- }
- }
-
- // General access - if you have a meaning, this will give you a (crossterm) style
- pub fn as_style(&self, meaning: Meaning) -> ContentStyle {
- self.styles[self.closest_meaning(&meaning)]
- }
-
- // Turns a map of meanings to colornames into a theme
- // If theme-debug is on, then we will print any colornames that we cannot load,
- // but we do not have this on in general, as it could print unfiltered text to the terminal
- // from a theme TOML file. However, it will always return a theme, falling back to
- // defaults on error, so that a TOML file does not break loading
- pub fn from_foreground_colors(
- name: String,
- parent: Option<&Theme>,
- foreground_colors: HashMap<Meaning, String>,
- debug: bool,
- ) -> Theme {
- let styles: HashMap<Meaning, ContentStyle> = foreground_colors
- .iter()
- .map(|(name, color)| {
- (
- *name,
- StyleFactory::from_fg_string(color).unwrap_or_else(|err| {
- if debug {
- log::warn!("Tried to load string as a color unsuccessfully: ({name}={color}) {err}");
- }
- ContentStyle::default()
- }),
- )
- })
- .collect();
- Theme::from_map(name, parent, &styles)
- }
-
- // Boil down a meaning-color hashmap into a theme, by taking the defaults
- // for any unknown colors
- fn from_map(
- name: String,
- parent: Option<&Theme>,
- overrides: &HashMap<Meaning, ContentStyle>,
- ) -> Theme {
- let styles = match parent {
- Some(theme) => Box::new(theme.styles.clone()),
- None => Box::new(DEFAULT_THEME.styles.clone()),
- }
- .iter()
- .map(|(name, color)| match overrides.get(name) {
- Some(value) => (*name, *value),
- None => (*name, *color),
- })
- .collect();
- Theme::new(name, parent.map(|p| p.name.clone()), styles)
- }
-}
-
-// Use palette to get a color from a string name, if possible
-fn from_string(name: &str) -> Result<Color, String> {
- if name.is_empty() {
- return Err("Empty string".into());
- }
- let first_char = name.chars().next().unwrap();
- match first_char {
- '#' => {
- let hexcode = &name[1..];
- let vec: Vec<u8> = hexcode
- .chars()
- .collect::<Vec<char>>()
- .chunks(2)
- .map(|pair| u8::from_str_radix(pair.iter().collect::<String>().as_str(), 16))
- .filter_map(|n| n.ok())
- .collect();
- if vec.len() != 3 {
- return Err("Could not parse 3 hex values from string".into());
- }
- Ok(Color::Rgb {
- r: vec[0],
- g: vec[1],
- b: vec[2],
- })
- }
- '@' => {
- // For full flexibility, we need to use serde_json, given
- // crossterm's approach.
- serde_json::from_str::<Color>(format!("\"{}\"", &name[1..]).as_str())
- .map_err(|_| format!("Could not convert color name {name} to Crossterm color"))
- }
- _ => {
- let srgb = named::from_str(name).ok_or("No such color in palette")?;
- Ok(Color::Rgb {
- r: srgb.red,
- g: srgb.green,
- b: srgb.blue,
- })
- }
- }
-}
-
-pub struct StyleFactory {}
-
-impl StyleFactory {
- fn from_fg_string(name: &str) -> Result<ContentStyle, String> {
- match from_string(name) {
- Ok(color) => Ok(Self::from_fg_color(color)),
- Err(err) => Err(err),
- }
- }
-
- // For succinctness, if we are confident that the name will be known,
- // this routine is available to keep the code readable
- fn known_fg_string(name: &str) -> ContentStyle {
- Self::from_fg_string(name).unwrap()
- }
-
- fn from_fg_color(color: Color) -> ContentStyle {
- ContentStyle {
- foreground_color: Some(color),
- ..ContentStyle::default()
- }
- }
-
- fn from_fg_color_and_attributes(color: Color, attributes: Attributes) -> ContentStyle {
- ContentStyle {
- foreground_color: Some(color),
- attributes,
- ..ContentStyle::default()
- }
- }
-}
-
-// Built-in themes. Rather than having extra files added before any theming
-// is available, this gives a couple of basic options, demonstrating the use
-// of themes: autumn and marine
-static ALERT_TYPES: LazyLock<HashMap<log::Level, Meaning>> = LazyLock::new(|| {
- HashMap::from([
- (log::Level::Info, Meaning::AlertInfo),
- (log::Level::Warn, Meaning::AlertWarn),
- (log::Level::Error, Meaning::AlertError),
- ])
-});
-
-static MEANING_FALLBACKS: LazyLock<HashMap<Meaning, Meaning>> = LazyLock::new(|| {
- HashMap::from([
- (Meaning::Guidance, Meaning::AlertInfo),
- (Meaning::Annotation, Meaning::AlertInfo),
- (Meaning::Title, Meaning::Important),
- ])
-});
-
-static DEFAULT_THEME: LazyLock<Theme> = LazyLock::new(|| {
- Theme::new(
- "default".to_string(),
- None,
- HashMap::from([
- (
- Meaning::AlertError,
- StyleFactory::from_fg_color(Color::DarkRed),
- ),
- (
- Meaning::AlertWarn,
- StyleFactory::from_fg_color(Color::DarkYellow),
- ),
- (
- Meaning::AlertInfo,
- StyleFactory::from_fg_color(Color::DarkGreen),
- ),
- (
- Meaning::Annotation,
- StyleFactory::from_fg_color(Color::DarkGrey),
- ),
- (
- Meaning::Guidance,
- StyleFactory::from_fg_color(Color::DarkBlue),
- ),
- (
- Meaning::Important,
- StyleFactory::from_fg_color_and_attributes(
- Color::White,
- Attributes::from(Attribute::Bold),
- ),
- ),
- (Meaning::Muted, StyleFactory::from_fg_color(Color::Grey)),
- (Meaning::Base, ContentStyle::default()),
- ]),
- )
-});
-
-static BUILTIN_THEMES: LazyLock<HashMap<&'static str, Theme>> = LazyLock::new(|| {
- HashMap::from([
- ("default", HashMap::new()),
- (
- "(none)",
- HashMap::from([
- (Meaning::AlertError, ContentStyle::default()),
- (Meaning::AlertWarn, ContentStyle::default()),
- (Meaning::AlertInfo, ContentStyle::default()),
- (Meaning::Annotation, ContentStyle::default()),
- (Meaning::Guidance, ContentStyle::default()),
- (Meaning::Important, ContentStyle::default()),
- (Meaning::Muted, ContentStyle::default()),
- (Meaning::Base, ContentStyle::default()),
- ]),
- ),
- (
- "autumn",
- HashMap::from([
- (
- Meaning::AlertError,
- StyleFactory::known_fg_string("saddlebrown"),
- ),
- (
- Meaning::AlertWarn,
- StyleFactory::known_fg_string("darkorange"),
- ),
- (Meaning::AlertInfo, StyleFactory::known_fg_string("gold")),
- (
- Meaning::Annotation,
- StyleFactory::from_fg_color(Color::DarkGrey),
- ),
- (Meaning::Guidance, StyleFactory::known_fg_string("brown")),
- ]),
- ),
- (
- "marine",
- HashMap::from([
- (
- Meaning::AlertError,
- StyleFactory::known_fg_string("yellowgreen"),
- ),
- (Meaning::AlertWarn, StyleFactory::known_fg_string("cyan")),
- (
- Meaning::AlertInfo,
- StyleFactory::known_fg_string("turquoise"),
- ),
- (
- Meaning::Annotation,
- StyleFactory::known_fg_string("steelblue"),
- ),
- (
- Meaning::Base,
- StyleFactory::known_fg_string("lightsteelblue"),
- ),
- (Meaning::Guidance, StyleFactory::known_fg_string("teal")),
- ]),
- ),
- ])
- .iter()
- .map(|(name, theme)| (*name, Theme::from_map(name.to_string(), None, theme)))
- .collect()
-});
-
-// To avoid themes being repeatedly loaded, we store them in a theme manager
-pub struct ThemeManager {
- loaded_themes: HashMap<String, Theme>,
- debug: bool,
- override_theme_dir: Option<String>,
-}
-
-// Theme-loading logic
-impl ThemeManager {
- pub fn new(debug: Option<bool>, theme_dir: Option<String>) -> Self {
- Self {
- loaded_themes: HashMap::new(),
- debug: debug.unwrap_or(false),
- override_theme_dir: match theme_dir {
- Some(theme_dir) => Some(theme_dir),
- None => std::env::var("ATUIN_THEME_DIR").ok(),
- },
- }
- }
-
- // Try to load a theme from a `{name}.toml` file in the theme directory. If an override is set
- // for the theme dir (via ATUIN_THEME_DIR env) we should load the theme from there
- pub fn load_theme_from_file(
- &mut self,
- name: &str,
- max_depth: u8,
- ) -> Result<&Theme, Box<dyn error::Error>> {
- let mut theme_file = if let Some(p) = &self.override_theme_dir {
- if p.is_empty() {
- return Err(Box::new(Error::new(
- ErrorKind::NotFound,
- "Empty theme directory override and could not find theme elsewhere",
- )));
- }
- PathBuf::from(p)
- } else {
- let config_dir = atuin_common::utils::config_dir();
- let mut theme_file = if let Ok(p) = std::env::var("ATUIN_CONFIG_DIR") {
- PathBuf::from(p)
- } else {
- let mut theme_file = PathBuf::new();
- theme_file.push(config_dir);
- theme_file
- };
- theme_file.push("themes");
- theme_file
- };
-
- let theme_toml = format!["{name}.toml"];
- theme_file.push(theme_toml);
-
- let mut config_builder = Config::builder();
-
- config_builder = config_builder.add_source(ConfigFile::new(
- theme_file.to_str().unwrap(),
- FileFormat::Toml,
- ));
-
- let config = config_builder.build()?;
- self.load_theme_from_config(name, config, max_depth)
- }
-
- pub fn load_theme_from_config(
- &mut self,
- name: &str,
- config: Config,
- max_depth: u8,
- ) -> Result<&Theme, Box<dyn error::Error>> {
- let debug = self.debug;
- let theme_config: ThemeConfig = match config.try_deserialize() {
- Ok(tc) => tc,
- Err(e) => {
- return Err(Box::new(Error::new(
- ErrorKind::InvalidInput,
- format!(
- "Failed to deserialize theme: {}",
- if debug {
- e.to_string()
- } else {
- "set theme debug on for more info".to_string()
- }
- ),
- )));
- }
- };
- let colors: HashMap<Meaning, String> = theme_config.colors;
- let parent: Option<&Theme> = match theme_config.theme.parent {
- Some(parent_name) => {
- if max_depth == 0 {
- return Err(Box::new(Error::new(
- ErrorKind::InvalidInput,
- "Parent requested but we hit the recursion limit",
- )));
- }
- Some(self.load_theme(parent_name.as_str(), Some(max_depth - 1)))
- }
- None => Some(self.load_theme("default", Some(max_depth - 1))),
- };
-
- if debug && name != theme_config.theme.name {
- log::warn!(
- "Your theme config name is not the name of your loaded theme {} != {}",
- name,
- theme_config.theme.name
- );
- }
-
- let theme = Theme::from_foreground_colors(theme_config.theme.name, parent, colors, debug);
- let name = name.to_string();
- self.loaded_themes.insert(name.clone(), theme);
- let theme = self.loaded_themes.get(&name).unwrap();
- Ok(theme)
- }
-
- // Check if the requested theme is loaded and, if not, then attempt to get it
- // from the builtins or, if not there, from file
- pub fn load_theme(&mut self, name: &str, max_depth: Option<u8>) -> &Theme {
- if self.loaded_themes.contains_key(name) {
- return self.loaded_themes.get(name).unwrap();
- }
- let built_ins = &BUILTIN_THEMES;
- match built_ins.get(name) {
- Some(theme) => theme,
- None => match self.load_theme_from_file(name, max_depth.unwrap_or(DEFAULT_MAX_DEPTH)) {
- Ok(theme) => theme,
- Err(err) => {
- log::warn!("Could not load theme {name}: {err}");
- built_ins.get("(none)").unwrap()
- }
- },
- }
- }
-}
-
-#[cfg(test)]
-mod theme_tests {
- use super::*;
-
- #[test]
- fn test_can_load_builtin_theme() {
- let mut manager = ThemeManager::new(Some(false), Some("".to_string()));
- let theme = manager.load_theme("autumn", None);
- assert_eq!(
- theme.as_style(Meaning::Guidance).foreground_color,
- from_string("brown").ok()
- );
- }
-
- #[test]
- fn test_can_create_theme() {
- let mut manager = ThemeManager::new(Some(false), Some("".to_string()));
- let mytheme = Theme::new(
- "mytheme".to_string(),
- None,
- HashMap::from([(
- Meaning::AlertError,
- StyleFactory::known_fg_string("yellowgreen"),
- )]),
- );
- manager.loaded_themes.insert("mytheme".to_string(), mytheme);
- let theme = manager.load_theme("mytheme", None);
- assert_eq!(
- theme.as_style(Meaning::AlertError).foreground_color,
- from_string("yellowgreen").ok()
- );
- }
-
- #[test]
- fn test_can_fallback_when_meaning_missing() {
- let mut manager = ThemeManager::new(Some(false), Some("".to_string()));
-
- // We use title as an example of a meaning that is not defined
- // even in the base theme.
- assert!(!DEFAULT_THEME.styles.contains_key(&Meaning::Title));
-
- let config = Config::builder()
- .add_source(ConfigFile::from_str(
- "
- [theme]
- name = \"title_theme\"
-
- [colors]
- Guidance = \"white\"
- AlertInfo = \"zomp\"
- ",
- FileFormat::Toml,
- ))
- .build()
- .unwrap();
- let theme = manager
- .load_theme_from_config("config_theme", config, 1)
- .unwrap();
-
- // Correctly picks overridden color.
- assert_eq!(
- theme.as_style(Meaning::Guidance).foreground_color,
- from_string("white").ok()
- );
-
- // Does not fall back to any color.
- assert_eq!(theme.as_style(Meaning::AlertInfo).foreground_color, None);
-
- // Even for the base.
- assert_eq!(theme.as_style(Meaning::Base).foreground_color, None);
-
- // Falls back to red as meaning missing from theme, so picks base default.
- assert_eq!(
- theme.as_style(Meaning::AlertError).foreground_color,
- Some(Color::DarkRed)
- );
-
- // Falls back to Important as Title not available.
- assert_eq!(
- theme.as_style(Meaning::Title).foreground_color,
- theme.as_style(Meaning::Important).foreground_color,
- );
-
- let title_config = Config::builder()
- .add_source(ConfigFile::from_str(
- "
- [theme]
- name = \"title_theme\"
-
- [colors]
- Title = \"white\"
- AlertInfo = \"zomp\"
- ",
- FileFormat::Toml,
- ))
- .build()
- .unwrap();
- let title_theme = manager
- .load_theme_from_config("title_theme", title_config, 1)
- .unwrap();
-
- assert_eq!(
- title_theme.as_style(Meaning::Title).foreground_color,
- Some(Color::White)
- );
- }
-
- #[test]
- fn test_no_fallbacks_are_circular() {
- let mytheme = Theme::new("mytheme".to_string(), None, HashMap::from([]));
- MEANING_FALLBACKS
- .iter()
- .for_each(|pair| assert_eq!(mytheme.closest_meaning(pair.0), &Meaning::Base))
- }
-
- #[test]
- fn test_can_get_colors_via_convenience_functions() {
- let mut manager = ThemeManager::new(Some(true), Some("".to_string()));
- let theme = manager.load_theme("default", None);
- assert_eq!(theme.get_error().foreground_color.unwrap(), Color::DarkRed);
- assert_eq!(
- theme.get_warning().foreground_color.unwrap(),
- Color::DarkYellow
- );
- assert_eq!(theme.get_info().foreground_color.unwrap(), Color::DarkGreen);
- assert_eq!(theme.get_base().foreground_color, None);
- assert_eq!(
- theme.get_alert(log::Level::Error).foreground_color.unwrap(),
- Color::DarkRed
- )
- }
-
- #[test]
- fn test_can_use_parent_theme_for_fallbacks() {
- testing_logger::setup();
-
- let mut manager = ThemeManager::new(Some(false), Some("".to_string()));
-
- // First, we introduce a base theme
- let solarized = Config::builder()
- .add_source(ConfigFile::from_str(
- "
- [theme]
- name = \"solarized\"
-
- [colors]
- Guidance = \"white\"
- AlertInfo = \"pink\"
- ",
- FileFormat::Toml,
- ))
- .build()
- .unwrap();
- let solarized_theme = manager
- .load_theme_from_config("solarized", solarized, 1)
- .unwrap();
-
- assert_eq!(
- solarized_theme
- .as_style(Meaning::AlertInfo)
- .foreground_color,
- from_string("pink").ok()
- );
-
- // Then we introduce a derived theme
- let unsolarized = Config::builder()
- .add_source(ConfigFile::from_str(
- "
- [theme]
- name = \"unsolarized\"
- parent = \"solarized\"
-
- [colors]
- AlertInfo = \"red\"
- ",
- FileFormat::Toml,
- ))
- .build()
- .unwrap();
- let unsolarized_theme = manager
- .load_theme_from_config("unsolarized", unsolarized, 1)
- .unwrap();
-
- // It will take its own values
- assert_eq!(
- unsolarized_theme
- .as_style(Meaning::AlertInfo)
- .foreground_color,
- from_string("red").ok()
- );
-
- // ...or fall back to the parent
- assert_eq!(
- unsolarized_theme
- .as_style(Meaning::Guidance)
- .foreground_color,
- from_string("white").ok()
- );
-
- testing_logger::validate(|captured_logs| assert_eq!(captured_logs.len(), 0));
-
- // If the parent is not found, we end up with the no theme colors or styling
- // as this is considered a (soft) error state.
- let nunsolarized = Config::builder()
- .add_source(ConfigFile::from_str(
- "
- [theme]
- name = \"nunsolarized\"
- parent = \"nonsolarized\"
-
- [colors]
- AlertInfo = \"red\"
- ",
- FileFormat::Toml,
- ))
- .build()
- .unwrap();
- let nunsolarized_theme = manager
- .load_theme_from_config("nunsolarized", nunsolarized, 1)
- .unwrap();
-
- assert_eq!(
- nunsolarized_theme
- .as_style(Meaning::Guidance)
- .foreground_color,
- None
- );
-
- testing_logger::validate(|captured_logs| {
- assert_eq!(captured_logs.len(), 1);
- assert_eq!(
- captured_logs[0].body,
- "Could not load theme nonsolarized: Empty theme directory override and could not find theme elsewhere"
- );
- assert_eq!(captured_logs[0].level, log::Level::Warn)
- });
- }
-
- #[test]
- fn test_can_debug_theme() {
- testing_logger::setup();
- [true, false].iter().for_each(|debug| {
- let mut manager = ThemeManager::new(Some(*debug), Some("".to_string()));
- let config = Config::builder()
- .add_source(ConfigFile::from_str(
- "
- [theme]
- name = \"mytheme\"
-
- [colors]
- Guidance = \"white\"
- AlertInfo = \"xinetic\"
- ",
- FileFormat::Toml,
- ))
- .build()
- .unwrap();
- manager
- .load_theme_from_config("config_theme", config, 1)
- .unwrap();
- testing_logger::validate(|captured_logs| {
- if *debug {
- assert_eq!(captured_logs.len(), 2);
- assert_eq!(
- captured_logs[0].body,
- "Your theme config name is not the name of your loaded theme config_theme != mytheme"
- );
- assert_eq!(captured_logs[0].level, log::Level::Warn);
- assert_eq!(
- captured_logs[1].body,
- "Tried to load string as a color unsuccessfully: (AlertInfo=xinetic) No such color in palette"
- );
- assert_eq!(captured_logs[1].level, log::Level::Warn)
- } else {
- assert_eq!(captured_logs.len(), 0)
- }
- })
- })
- }
-
- #[test]
- fn test_can_parse_color_strings_correctly() {
- assert_eq!(
- from_string("brown").unwrap(),
- Color::Rgb {
- r: 165,
- g: 42,
- b: 42
- }
- );
-
- assert_eq!(from_string(""), Err("Empty string".into()));
-
- ["manatee", "caput mortuum", "123456"]
- .iter()
- .for_each(|inp| {
- assert_eq!(from_string(inp), Err("No such color in palette".into()));
- });
-
- assert_eq!(
- from_string("#ff1122").unwrap(),
- Color::Rgb {
- r: 255,
- g: 17,
- b: 34
- }
- );
- ["#1122", "#ffaa112", "#brown"].iter().for_each(|inp| {
- assert_eq!(
- from_string(inp),
- Err("Could not parse 3 hex values from string".into())
- );
- });
-
- assert_eq!(from_string("@dark_grey").unwrap(), Color::DarkGrey);
- assert_eq!(
- from_string("@rgb_(255,255,255)").unwrap(),
- Color::Rgb {
- r: 255,
- g: 255,
- b: 255
- }
- );
- assert_eq!(from_string("@ansi_(255)").unwrap(), Color::AnsiValue(255));
- ["@", "@DarkGray", "@Dark 4ay", "@ansi(256)"]
- .iter()
- .for_each(|inp| {
- assert_eq!(
- from_string(inp),
- Err(format!(
- "Could not convert color name {inp} to Crossterm color"
- ))
- );
- });
- }
-}