/* * meli - themes conf module * * Copyright 2019 Manos Pitsidianakis * * This file is part of meli. * * meli is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * meli is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with meli. If not, see . */ //! Application themes. //! //! * An attribute is a triple of foreground color, background color and terminal attribute `ThemeValue`s. //! * A `ThemeValue` is either an actual value or the key name of another value to which it depends. The value is either `Color` or `Attr`. //! * `ThemeAttributeInner` is an attribute triplet. //! * `ThemeAttribute` is an attribute triplet with the links resolved. //! //! On startup a [DFS](https://en.wikipedia.org/wiki/Depth-first_search) is performed to see if there are any cycles in the link graph. use crate::terminal::{Attr, Color}; use crate::Context; use indexmap::IndexMap; use melib::{MeliError, Result}; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use smallvec::SmallVec; use std::borrow::Cow; use std::collections::HashSet; #[inline(always)] pub fn value(context: &Context, key: &'static str) -> ThemeAttribute { let theme = match context.settings.terminal.theme.as_str() { "light" => &context.settings.terminal.themes.light, "dark" => &context.settings.terminal.themes.dark, t => context .settings .terminal .themes .other_themes .get(t) .unwrap_or(&context.settings.terminal.themes.dark), }; unlink(theme, &Cow::from(key)) } #[inline(always)] pub fn fg_color(context: &Context, key: &'static str) -> Color { let theme = match context.settings.terminal.theme.as_str() { "light" => &context.settings.terminal.themes.light, "dark" => &context.settings.terminal.themes.dark, t => context .settings .terminal .themes .other_themes .get(t) .unwrap_or(&context.settings.terminal.themes.dark), }; unlink_fg(theme, &ColorField::Fg, &Cow::from(key)) } #[inline(always)] pub fn bg_color(context: &Context, key: &'static str) -> Color { let theme = match context.settings.terminal.theme.as_str() { "light" => &context.settings.terminal.themes.light, "dark" => &context.settings.terminal.themes.dark, t => context .settings .terminal .themes .other_themes .get(t) .unwrap_or(&context.settings.terminal.themes.dark), }; unlink_bg(theme, &ColorField::Bg, &Cow::from(key)) } #[inline(always)] pub fn attrs(context: &Context, key: &'static str) -> Attr { let theme = match context.settings.terminal.theme.as_str() { "light" => &context.settings.terminal.themes.light, "dark" => &context.settings.terminal.themes.dark, t => context .settings .terminal .themes .other_themes .get(t) .unwrap_or(&context.settings.terminal.themes.dark), }; unlink_attrs(theme, &Cow::from(key)) } #[inline(always)] fn unlink<'k, 't: 'k>(theme: &'t Theme, key: &'k Cow<'static, str>) -> ThemeAttribute { ThemeAttribute { fg: unlink_fg(theme, &ColorField::Fg, key), bg: unlink_bg(theme, &ColorField::Bg, key), attrs: unlink_attrs(theme, key), } } #[inline(always)] fn unlink_fg<'k, 't: 'k>( theme: &'t Theme, mut field: &'k ColorField, mut key: &'k Cow<'static, str>, ) -> Color { loop { match field { ColorField::LikeSelf | ColorField::Fg => match &theme[key].fg { ThemeValue::Link(ref new_key, ref new_field) => { key = new_key; field = new_field } ThemeValue::Alias(ref alias_ident) => { let mut alias_ident = alias_ident; 'self_alias_loop: loop { match &theme.color_aliases[alias_ident.as_ref()] { ThemeValue::Link(ref new_key, ref new_field) => { key = new_key; field = new_field; break 'self_alias_loop; } ThemeValue::Alias(ref new_alias_ident) => alias_ident = new_alias_ident, ThemeValue::Value(val) => return *val, } } } ThemeValue::Value(val) => return *val, }, ColorField::Bg => match &theme[key].bg { ThemeValue::Link(ref new_key, ref new_field) => { key = new_key; field = new_field } ThemeValue::Alias(ref alias_ident) => { let mut alias_ident = alias_ident; 'other_alias_loop: loop { match &theme.color_aliases[alias_ident.as_ref()] { ThemeValue::Link(ref new_key, ref new_field) => { key = new_key; field = new_field; break 'other_alias_loop; } ThemeValue::Alias(ref new_alias_ident) => alias_ident = new_alias_ident, ThemeValue::Value(val) => return *val, } } } ThemeValue::Value(val) => return *val, }, } } } #[inline(always)] fn unlink_bg<'k, 't: 'k>( theme: &'t Theme, mut field: &'k ColorField, mut key: &'k Cow<'static, str>, ) -> Color { loop { match field { ColorField::LikeSelf | ColorField::Bg => match &theme[key].bg { ThemeValue::Link(ref new_key, ref new_field) => { key = new_key; field = new_field } ThemeValue::Alias(ref alias_ident) => { let mut alias_ident = alias_ident; 'self_alias_loop: loop { match &theme.color_aliases[alias_ident.as_ref()] { ThemeValue::Link(ref new_key, ref new_field) => { key = new_key; field = new_field; break 'self_alias_loop; } ThemeValue::Alias(ref new_alias_ident) => alias_ident = new_alias_ident, ThemeValue::Value(val) => return *val, } } } ThemeValue::Value(val) => return *val, }, ColorField::Fg => match &theme[key].fg { ThemeValue::Link(ref new_key, ref new_field) => { key = new_key; field = new_field } ThemeValue::Alias(ref alias_ident) => { let mut alias_ident = alias_ident; 'other_alias_loop: loop { match &theme.color_aliases[alias_ident.as_ref()] { ThemeValue::Link(ref new_key, ref new_field) => { key = new_key; field = new_field; break 'other_alias_loop; } ThemeValue::Alias(ref new_alias_ident) => alias_ident = new_alias_ident, ThemeValue::Value(val) => return *val, } } } ThemeValue::Value(val) => return *val, }, } } } #[inline(always)] fn unlink_attrs<'k, 't: 'k>(theme: &'t Theme, mut key: &'k Cow<'static, str>) -> Attr { loop { match &theme[key].attrs { ThemeValue::Link(ref new_key, ()) => key = new_key, ThemeValue::Alias(ref alias_ident) => { let mut alias_ident = alias_ident; 'alias_loop: loop { match &theme.attr_aliases[alias_ident.as_ref()] { ThemeValue::Link(ref new_key, ()) => { key = new_key; break 'alias_loop; } ThemeValue::Alias(ref new_alias_ident) => alias_ident = new_alias_ident, ThemeValue::Value(val) => return *val, } } } ThemeValue::Value(val) => return *val, } } } const DEFAULT_KEYS: &[&str] = &[ "theme_default", "status.bar", "status.notification", "tab.focused", "tab.unfocused", "tab.bar", "widgets.list.header", "widgets.form.label", "widgets.form.field", "widgets.form.highlighted", "widgets.options.highlighted", "mail.sidebar", "mail.sidebar_divider", "mail.sidebar_account_name", "mail.sidebar_unread_count", "mail.sidebar_index", "mail.sidebar_highlighted", "mail.sidebar_highlighted_account_name", "mail.sidebar_highlighted_unread_count", "mail.sidebar_highlighted_index", "mail.sidebar_highlighted_account", "mail.sidebar_highlighted_account_unread_count", "mail.sidebar_highlighted_account_index", "mail.listing.compact.even", "mail.listing.compact.odd", "mail.listing.compact.even_unseen", "mail.listing.compact.odd_unseen", "mail.listing.compact.even_selected", "mail.listing.compact.odd_selected", "mail.listing.compact.even_highlighted", "mail.listing.compact.odd_highlighted", "mail.listing.plain.even", "mail.listing.plain.odd", "mail.listing.plain.even_unseen", "mail.listing.plain.odd_unseen", "mail.listing.plain.even_selected", "mail.listing.plain.odd_selected", "mail.listing.plain.even_highlighted", "mail.listing.plain.odd_highlighted", "mail.listing.conversations", "mail.listing.conversations.subject", "mail.listing.conversations.from", "mail.listing.conversations.date", "mail.listing.conversations.padding", "mail.listing.conversations.unseen", "mail.listing.conversations.unseen_padding", "mail.listing.conversations.highlighted", "mail.listing.conversations.selected", "mail.view.headers", "mail.view.headers_names", "mail.view.headers_area", "mail.view.body", "mail.view.thread.indentation.a", "mail.view.thread.indentation.b", "mail.view.thread.indentation.c", "mail.view.thread.indentation.d", "mail.view.thread.indentation.e", "mail.view.thread.indentation.f", "mail.listing.attachment_flag", "mail.listing.thread_snooze_flag", "mail.listing.tag_default", "pager.highlight_search", "pager.highlight_search_current", ]; /// `ThemeAttributeInner` but with the links resolved. #[derive(Debug, PartialEq, Eq, Clone, Default, Copy, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ThemeAttribute { pub fg: Color, pub bg: Color, pub attrs: Attr, } /// Holds {fore,back}ground color and terminal attribute values. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ThemeAttributeInner { #[serde(default)] fg: ThemeValue, #[serde(default)] bg: ThemeValue, #[serde(default)] attrs: ThemeValue, } impl Default for ThemeAttributeInner { fn default() -> Self { Self { fg: "theme_default".into(), bg: "theme_default".into(), attrs: "theme_default".into(), } } } #[derive(Debug, Clone)] pub enum ColorField { // Like self, i.e. either Fg or Bg LikeSelf, Fg, Bg, } /// The field a ThemeValue::Link refers to. trait ThemeLink { type LinkType; } /// A color value that's a link can either refer to .fg or .bg field impl ThemeLink for Color { type LinkType = ColorField; } /// An attr value that's a link can only refer to an .attr field impl ThemeLink for Attr { type LinkType = (); } #[derive(Debug, Clone)] /// Holds either an actual value or refers to the key name of the attribute that holds the value. enum ThemeValue { Value(T), Alias(Cow<'static, str>), Link(Cow<'static, str>, T::LinkType), } impl From<&'static str> for ThemeValue { fn from(from: &'static str) -> Self { ThemeValue::Link(from.into(), ColorField::LikeSelf) } } impl From<&'static str> for ThemeValue { fn from(from: &'static str) -> Self { ThemeValue::Link(from.into(), ()) } } impl From for ThemeValue { fn from(from: Color) -> Self { ThemeValue::Value(from) } } impl From for ThemeValue { fn from(from: Attr) -> Self { ThemeValue::Value(from) } } impl Default for ThemeValue { fn default() -> Self { ThemeValue::Value(Color::Default) } } impl Default for ThemeValue { fn default() -> Self { ThemeValue::Value(Attr::DEFAULT) } } impl<'de> Deserialize<'de> for ThemeValue { fn deserialize(deserializer: D) -> std::result::Result where D: Deserializer<'de>, { if let Ok(s) = ::deserialize(deserializer) { if s.starts_with("$") { Ok(ThemeValue::Alias(s[1..].to_string().into())) } else if let Ok(c) = Attr::from_string_de::<'de, D, String>(s.clone()) { Ok(ThemeValue::Value(c)) } else { Ok(ThemeValue::Link(s.into(), ())) } } else { Err(de::Error::custom("invalid theme attribute value")) } } } impl Serialize for ThemeValue { fn serialize(&self, serializer: S) -> std::result::Result where S: Serializer, { match self { ThemeValue::Value(s) => s.serialize(serializer), ThemeValue::Alias(s) => format!("${}", s).serialize(serializer), ThemeValue::Link(s, ()) => serializer.serialize_str(s.as_ref()), } } } impl Serialize for ThemeValue { fn serialize(&self, serializer: S) -> std::result::Result where S: Serializer, { match self { ThemeValue::Value(s) => s.serialize(serializer), ThemeValue::Alias(s) => format!("${}", s).serialize(serializer), ThemeValue::Link(s, ColorField::LikeSelf) => serializer.serialize_str(s.as_ref()), ThemeValue::Link(s, ColorField::Fg) => { serializer.serialize_str(format!("{}.fg", s).as_ref()) } ThemeValue::Link(s, ColorField::Bg) => { serializer.serialize_str(format!("{}.bg", s).as_ref()) } } } } impl<'de> Deserialize<'de> for ThemeValue { fn deserialize(deserializer: D) -> std::result::Result where D: Deserializer<'de>, { if let Ok(s) = ::deserialize(deserializer) { if s.starts_with("$") { Ok(ThemeValue::Alias(s[1..].to_string().into())) } else if let Ok(c) = Color::from_string_de::<'de, D>(s.clone()) { Ok(ThemeValue::Value(c)) } else if s.ends_with(".fg") { Ok(ThemeValue::Link( s[..s.len() - 3].to_string().into(), ColorField::Fg, )) } else if s.ends_with(".bg") { Ok(ThemeValue::Link( s[..s.len() - 3].to_string().into(), ColorField::Bg, )) } else { Ok(ThemeValue::Link(s.into(), ColorField::LikeSelf)) } } else { Err(de::Error::custom("invalid theme color value")) } } } #[derive(Debug, Clone)] pub struct Themes { pub light: Theme, pub dark: Theme, pub other_themes: IndexMap, } #[derive(Debug, Clone)] pub struct Theme { color_aliases: IndexMap, ThemeValue>, attr_aliases: IndexMap, ThemeValue>, #[cfg(feature = "regexp")] text_format_regexps: IndexMap, SmallVec<[TextFormatterSetting; 32]>>, pub keys: IndexMap, ThemeAttributeInner>, } #[cfg(feature = "regexp")] pub use regexp::text_format_regexps; #[cfg(feature = "regexp")] use regexp::*; #[cfg(feature = "regexp")] mod regexp { use super::*; use crate::terminal::FormatTag; pub(super) const DEFAULT_TEXT_FORMATTER_KEYS: &[&str] = &["pager.envelope.body", "listing.from", "listing.subject"]; #[derive(Clone)] pub struct RegexpWrapper(pub pcre2::bytes::Regex); #[derive(Debug, Clone)] pub(super) struct TextFormatterSetting { pub(super) regexp: RegexpWrapper, pub(super) fg: Option>, pub(super) bg: Option>, pub(super) attrs: Option>, pub(super) priority: u8, } #[derive(Debug, Clone)] pub struct TextFormatter<'r> { pub regexp: &'r RegexpWrapper, pub tag: FormatTag, } impl Default for RegexpWrapper { fn default() -> Self { Self(pcre2::bytes::Regex::new("").unwrap()) } } impl std::fmt::Debug for RegexpWrapper { fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { std::fmt::Debug::fmt(self.0.as_str(), fmt) } } impl std::hash::Hash for RegexpWrapper { fn hash(&self, state: &mut H) { self.0.as_str().hash(state) } } impl Eq for RegexpWrapper {} impl PartialEq for RegexpWrapper { fn eq(&self, other: &RegexpWrapper) -> bool { self.0.as_str().eq(other.0.as_str()) } } impl RegexpWrapper { pub(super) fn new( pattern: &str, caseless: bool, dotall: bool, extended: bool, multi_line: bool, ucp: bool, jit_if_available: bool, ) -> std::result::Result { Ok(Self(unsafe { pcre2::bytes::RegexBuilder::new() .caseless(caseless) .dotall(dotall) .extended(extended) .multi_line(multi_line) .ucp(ucp) .jit_if_available(jit_if_available) .disable_utf_check() // We only match on rust strings, which are guaranteed UTF8 .build(pattern)? })) } pub fn find_iter<'w, 's>(&'w self, s: &'s str) -> FindIter<'w, 's> { FindIter { pcre_iter: self.0.find_iter(s.as_bytes()), char_indices: s.char_indices(), char_offset: 0, } } } pub struct FindIter<'r, 's> { pcre_iter: pcre2::bytes::Matches<'r, 's>, char_indices: std::str::CharIndices<'s>, char_offset: usize, } impl<'r, 's> Iterator for FindIter<'r, 's> { type Item = (usize, usize); fn next(&mut self) -> Option { loop { let next_byte_offset = self.pcre_iter.next(); if next_byte_offset.is_none() { return None; } let next_byte_offset = next_byte_offset.unwrap(); if next_byte_offset.is_err() { continue; } let next_byte_offset = next_byte_offset.unwrap(); let mut next_char_index = self.char_indices.next(); if next_char_index.is_none() { return None; } while next_byte_offset.start() < next_char_index.unwrap().0 { self.char_offset += 1; next_char_index = self.char_indices.next(); if next_char_index.is_none() { return None; } } let start = self.char_offset; while next_byte_offset.end() >= self .char_indices .next() .map(|(v, _)| v) .unwrap_or(next_byte_offset.end()) + 1 { self.char_offset += 1; } let end = self.char_offset + 1; return Some((start, end)); } } } #[inline(always)] pub fn text_format_regexps<'ctx>( context: &'ctx Context, key: &'static str, ) -> SmallVec<[TextFormatter<'ctx>; 64]> { let theme = match context.settings.terminal.theme.as_str() { "light" => &context.settings.terminal.themes.light, "dark" => &context.settings.terminal.themes.dark, t => context .settings .terminal .themes .other_themes .get(t) .unwrap_or(&context.settings.terminal.themes.dark), }; theme.text_format_regexps[&Cow::from(key)] .iter() .map(|v| TextFormatter { regexp: &v.regexp, tag: FormatTag { fg: v.fg.as_ref().map(|v| match v { ThemeValue::Link(ref key, ref field) => unlink_fg(theme, field, key), ThemeValue::Alias(ref alias_ident) => { let mut alias_ident = alias_ident; let ret; 'fg_alias_loop: loop { match &theme.color_aliases[alias_ident.as_ref()] { ThemeValue::Link(ref new_key, ref new_field) => { ret = unlink_fg(theme, new_field, new_key); break 'fg_alias_loop; } ThemeValue::Alias(ref new_alias_ident) => { alias_ident = new_alias_ident } ThemeValue::Value(val) => { ret = *val; break 'fg_alias_loop; } } } ret } ThemeValue::Value(val) => *val, }), bg: v.bg.as_ref().map(|v| match v { ThemeValue::Link(ref key, ref field) => unlink_bg(theme, field, key), ThemeValue::Alias(ref alias_ident) => { let mut alias_ident = alias_ident; let ret; 'bg_alias_loop: loop { match &theme.color_aliases[alias_ident.as_ref()] { ThemeValue::Link(ref new_key, ref new_field) => { ret = unlink_bg(theme, new_field, new_key); break 'bg_alias_loop; } ThemeValue::Alias(ref new_alias_ident) => { alias_ident = new_alias_ident } ThemeValue::Value(val) => { ret = *val; break 'bg_alias_loop; } } } ret } ThemeValue::Value(val) => *val, }), attrs: v.attrs.as_ref().map(|v| match v { ThemeValue::Link(ref key, ()) => unlink_attrs(theme, key), ThemeValue::Alias(ref alias_ident) => { let mut alias_ident = alias_ident; let ret; 'attrs_alias_loop: loop { match &theme.attr_aliases[alias_ident.as_ref()] { ThemeValue::Link(ref new_key, ()) => { ret = unlink_attrs(theme, new_key); break 'attrs_alias_loop; } ThemeValue::Alias(ref new_alias_ident) => { alias_ident = new_alias_ident } ThemeValue::Value(val) => { ret = *val; break 'attrs_alias_loop; } } } ret } ThemeValue::Value(val) => *val, }), priority: v.priority, }, }) .collect() } } use std::ops::{Deref, DerefMut}; impl Deref for Theme { type Target = IndexMap, ThemeAttributeInner>; fn deref(&self) -> &Self::Target { &self.keys } } impl DerefMut for Theme { fn deref_mut(&mut self) -> &mut IndexMap, ThemeAttributeInner> { &mut self.keys } } impl<'de> Deserialize<'de> for Themes { fn deserialize(deserializer: D) -> std::result::Result where D: Deserializer<'de>, { #[cfg(feature = "regexp")] const fn false_val() -> bool { false } #[cfg(feature = "regexp")] const fn true_val() -> bool { true } #[derive(Deserialize)] struct ThemesOptions { #[serde(default)] light: ThemeOptions, #[serde(default)] dark: ThemeOptions, #[serde(flatten, default)] other_themes: IndexMap, } #[derive(Deserialize, Default)] struct ThemeOptions { #[serde(default)] color_aliases: IndexMap, ThemeValue>, #[serde(default)] attr_aliases: IndexMap, ThemeValue>, #[cfg(feature = "regexp")] #[serde(default)] text_format_regexps: IndexMap, IndexMap>, #[serde(flatten, default)] keys: IndexMap, ThemeAttributeInnerOptions>, } #[cfg(feature = "regexp")] #[derive(Deserialize, Default)] struct RegexpOptions { #[serde(default = "false_val")] caseless: bool, #[serde(default = "false_val")] dotall: bool, #[serde(default = "false_val")] extended: bool, #[serde(default = "false_val")] multi_line: bool, #[serde(default = "true_val")] ucp: bool, #[serde(default = "false_val")] jit_if_available: bool, #[serde(default)] priority: u8, #[serde(flatten)] rest: ThemeAttributeInnerOptions, } #[derive(Deserialize, Default)] #[serde(deny_unknown_fields)] struct ThemeAttributeInnerOptions { #[serde(default)] fg: Option>, #[serde(default)] bg: Option>, #[serde(default)] attrs: Option>, } let mut ret = Themes::default(); let mut s = ::deserialize(deserializer)?; for tk in s.other_themes.keys() { ret.other_themes.insert(tk.clone(), ret.dark.clone()); } for (k, v) in ret.light.iter_mut() { if let Some(mut att) = s.light.keys.remove(k) { if let Some(att) = att.fg.take() { v.fg = att; } if let Some(att) = att.bg.take() { v.bg = att; } if let Some(att) = att.attrs.take() { v.attrs = att; } } } if !s.light.keys.is_empty() { return Err(de::Error::custom(format!( "light theme contains unrecognized theme keywords: {}", s.light .keys .keys() .into_iter() .map(|k| k.as_ref()) .collect::>() .join(", ") ))); } ret.light.color_aliases = s.light.color_aliases; ret.light.attr_aliases = s.light.attr_aliases; #[cfg(feature = "regexp")] for (k, v) in s.light.text_format_regexps { let mut acc = SmallVec::new(); for (rs, v) in v { match RegexpWrapper::new( &rs, v.caseless, v.dotall, v.extended, v.multi_line, v.ucp, v.jit_if_available, ) { Ok(regexp) => { acc.push(TextFormatterSetting { regexp, fg: v.rest.fg, bg: v.rest.bg, attrs: v.rest.attrs, priority: v.priority, }); } Err(err) => { return Err(de::Error::custom(err.to_string())); } } } ret.light.text_format_regexps.insert(k, acc); } for (k, v) in ret.dark.iter_mut() { if let Some(mut att) = s.dark.keys.remove(k) { if let Some(att) = att.fg.take() { v.fg = att; } if let Some(att) = att.bg.take() { v.bg = att; } if let Some(att) = att.attrs.take() { v.attrs = att; } } } if !s.dark.keys.is_empty() { return Err(de::Error::custom(format!( "dark theme contains unrecognized theme keywords: {}", s.dark .keys .keys() .into_iter() .map(|k| k.as_ref()) .collect::>() .join(", ") ))); } ret.dark.color_aliases = s.dark.color_aliases; ret.dark.attr_aliases = s.dark.attr_aliases; #[cfg(feature = "regexp")] for (k, v) in s.dark.text_format_regexps { let mut acc = SmallVec::new(); for (rs, v) in v { match RegexpWrapper::new( &rs, v.caseless, v.dotall, v.extended, v.multi_line, v.ucp, v.jit_if_available, ) { Ok(regexp) => { acc.push(TextFormatterSetting { regexp, fg: v.rest.fg, bg: v.rest.bg, attrs: v.rest.attrs, priority: v.priority, }); } Err(err) => { return Err(de::Error::custom(err.to_string())); } } } ret.dark.text_format_regexps.insert(k, acc); } for (tk, t) in ret.other_themes.iter_mut() { let mut theme = s.other_themes.remove(tk).unwrap(); for (k, v) in t.iter_mut() { if let Some(mut att) = theme.keys.remove(k) { if let Some(att) = att.fg.take() { v.fg = att; } if let Some(att) = att.bg.take() { v.bg = att; } if let Some(att) = att.attrs.take() { v.attrs = att; } } } if !theme.keys.is_empty() { return Err(de::Error::custom(format!( "{} theme contains unrecognized theme keywords: {}", tk, theme .keys .keys() .into_iter() .map(|k| k.as_ref()) .collect::>() .join(", ") ))); } t.color_aliases = theme.color_aliases; t.attr_aliases = theme.attr_aliases; #[cfg(feature = "regexp")] for (k, v) in theme.text_format_regexps { let mut acc = SmallVec::new(); for (rs, v) in v { match RegexpWrapper::new( &rs, v.caseless, v.dotall, v.extended, v.multi_line, v.ucp, v.jit_if_available, ) { Ok(regexp) => { acc.push(TextFormatterSetting { regexp, fg: v.rest.fg, bg: v.rest.bg, attrs: v.rest.attrs, priority: v.priority, }); } Err(err) => { return Err(de::Error::custom(err.to_string())); } } } t.text_format_regexps.insert(k, acc); } } Ok(ret) } } impl Themes { fn validate_keys(name: &str, theme: &Theme, hash_set: &HashSet<&'static str>) -> Result<()> { let mut keys = theme .keys() .filter_map(|k| { if !hash_set.contains(&k.as_ref()) { Some((None, "key", "invalid key", k.as_ref())) } else { None } }) .chain(theme.color_aliases.iter().filter_map(|(key, a)| match a { ThemeValue::Link(ref r, ref field) => { if !hash_set.contains(&r.as_ref()) { Some(( Some(key), match field { ColorField::LikeSelf => "Color alias link", ColorField::Fg => "Color alias fg link", ColorField::Bg => "Color alias bg link", }, "invalid key", r.as_ref(), )) } else { None } } ThemeValue::Alias(ref ident) => { if !theme.color_aliases.contains_key(ident.as_ref()) { Some((Some(key), "alias", "nonexistant color alias", ident)) } else { None } } _ => None, })) .chain(theme.attr_aliases.iter().filter_map(|(key, a)| match a { ThemeValue::Link(ref r, ()) => { if !hash_set.contains(&r.as_ref()) { Some((Some(key), "Attr alias link", "invalid key", r.as_ref())) } else { None } } ThemeValue::Alias(ref ident) => { if !theme.attr_aliases.contains_key(ident.as_ref()) { Some((Some(key), "alias", "nonexistant color alias", ident)) } else { None } } _ => None, })) .chain(theme.iter().filter_map(|(key, a)| { if let ThemeValue::Link(ref r, _) = a.fg { if !hash_set.contains(&r.as_ref()) { Some((Some(key), "fg link", "invalid key", r.as_ref())) } else { None } } else if let ThemeValue::Alias(ref ident) = a.fg { if !theme.color_aliases.contains_key(ident.as_ref()) { Some((Some(key), "fg alias", "nonexistant color alias", ident)) } else { None } } else { None } })) .chain(theme.iter().filter_map(|(key, a)| { if let ThemeValue::Link(ref r, _) = a.bg { if !hash_set.contains(&r.as_ref()) { Some((Some(key), "bg link", "invalid key", r.as_ref())) } else { None } } else if let ThemeValue::Alias(ref ident) = a.bg { if !theme.color_aliases.contains_key(ident.as_ref()) { Some((Some(key), "bg alias", "nonexistant color alias", ident)) } else { None } } else { None } })) .chain(theme.iter().filter_map(|(key, a)| { if let ThemeValue::Link(ref r, _) = a.attrs { if !hash_set.contains(&r.as_ref()) { Some((Some(key), "attrs link", "invalid key", r.as_ref())) } else { None } } else if let ThemeValue::Alias(ref ident) = a.attrs { if !theme.attr_aliases.contains_key(ident.as_ref()) { Some(( Some(key), "attrs alias", "nonexistant text attribute alias", ident, )) } else { None } } else { None } })) .collect::, &'_ str, &'_ str, &'_ str); 128]>>(); #[cfg(feature = "regexp")] { for (key, v) in &theme.text_format_regexps { if !regexp::DEFAULT_TEXT_FORMATTER_KEYS.contains(&key.as_ref()) { keys.push(( None, "key", "invalid key in `text_format_regexps`", key.as_ref(), )); } else { for tfs in v { if let Some(fg) = &tfs.fg { if let ThemeValue::Link(ref r, _) = fg { if !hash_set.contains(&r.as_ref()) { keys.push(( Some(key), "fg link", "invalid key in `text_format_regexps`", r.as_ref(), )); } } else if let ThemeValue::Alias(ref ident) = fg { if !theme.color_aliases.contains_key(ident.as_ref()) { keys.push(( Some(key), "fg alias", "nonexistant color alias in `text_format_regexps`", ident, )); } } } if let Some(bg) = &tfs.bg { if let ThemeValue::Link(ref r, _) = bg { if !hash_set.contains(&r.as_ref()) { keys.push(( Some(key), "bg link", "invalid key in `text_format_regexps`", r.as_ref(), )); } } else if let ThemeValue::Alias(ref ident) = bg { if !theme.color_aliases.contains_key(ident.as_ref()) { keys.push(( Some(key), "bg alias", "nonexistant color alias in `text_format_regexps`", ident, )); } } } if let Some(attrs) = &tfs.attrs { if let ThemeValue::Link(ref r, _) = attrs { if !hash_set.contains(&r.as_ref()) { keys.push(( Some(key), "attrs link", "invalid key in `text_format_regexps`", r.as_ref(), )); } } else if let ThemeValue::Alias(ref ident) = attrs { if !theme.attr_aliases.contains_key(ident.as_ref()) { keys.push(( Some(key), "attrs alias", "nonexistant text attribute alias in `text_format_regexps`", ident, )); } } } } } } } if !keys.is_empty() { return Err(format!( "{} theme contains invalid data: {}", name, keys.into_iter() .map(|(key_opt, desc, kind, link)| if let Some(key) = key_opt { format!("{} {}: {} \"{}\"", key, desc, kind, link) } else { format!("{}: {} \"{}\"", desc, kind, link) }) .collect::>() .join(", ") ) .into()); } Ok(()) } pub fn validate(&self) -> Result<()> { let hash_set: HashSet<&'static str> = DEFAULT_KEYS.into_iter().map(|k| *k).collect(); Themes::validate_keys("light", &self.light, &hash_set)?; Themes::validate_keys("dark", &self.dark, &hash_set)?; for (name, t) in self.other_themes.iter() { Themes::validate_keys(name, t, &hash_set)?; } if let Err(err) = is_cyclic(&self.light) { return Err(MeliError::new(format!( "light theme contains a cycle: {}", err ))); } if let Err(err) = is_cyclic(&self.dark) { return Err(MeliError::new(format!( "dark theme contains a cycle: {}", err ))); } for (k, t) in self.other_themes.iter() { if let Err(err) = is_cyclic(t) { return Err(MeliError::new(format!( "{} theme contains a cycle: {}", k, err ))); } } Ok(()) } pub fn key_to_string(&self, key: &str, unlink: bool) -> String { let theme = match key { "light" => &self.light, "dark" => &self.dark, t => self.other_themes.get(t).unwrap_or(&self.dark), }; let mut ret = String::new(); ret.extend(format!("[terminal.themes.{}]\n", key).chars()); if unlink { for k in theme.keys() { ret.extend( format!( "\"{}\" = {{ fg = {}, bg = {}, attrs = {} }}\n", k, toml::to_string(&unlink_fg(&theme, &ColorField::Fg, k)).unwrap(), toml::to_string(&unlink_bg(&theme, &ColorField::Bg, k)).unwrap(), toml::to_string(&unlink_attrs(&theme, k)).unwrap(), ) .chars(), ); } } else { for k in theme.keys() { ret.extend( format!( "\"{}\" = {{ fg = {}, bg = {}, attrs = {} }}\n", k, toml::to_string(&theme[k].fg).unwrap(), toml::to_string(&theme[k].bg).unwrap(), toml::to_string(&theme[k].attrs).unwrap(), ) .chars(), ); } } ret } pub fn to_string(&self) -> String { let mut ret = String::new(); ret.extend(self.key_to_string("dark", true).chars()); ret.push_str("\n\n"); ret.extend(self.key_to_string("light", true).chars()); for name in self.other_themes.keys() { ret.push_str("\n\n"); ret.extend(self.key_to_string(name, true).chars()); } ret } } impl Default for Themes { fn default() -> Themes { let mut light = IndexMap::default(); let mut dark = IndexMap::default(); let other_themes = IndexMap::default(); macro_rules! add { ($key:literal, $($theme:ident={ $($name:ident : $val:expr),*$(,)? }),*$(,)?) => { add!($key); $($theme.insert($key.into(), ThemeAttributeInner { $($name: $val.into()),* ,..ThemeAttributeInner::default() }));* }; ($key:literal) => { light.insert($key.into(), ThemeAttributeInner::default()); dark.insert($key.into(), ThemeAttributeInner::default()); }; } add!("theme_default", dark = { fg: Color::Default, bg: Color::Default, attrs: Attr::DEFAULT }, light = { fg: Color::Default, bg: Color::Default, attrs: Attr::DEFAULT }); add!("status.bar", dark = { fg: Color::Byte(123), bg: Color::Byte(26) }, light = { fg: Color::Byte(123), bg: Color::Byte(26) }); add!("status.notification", dark = { fg: Color::Byte(219), bg: Color::Default }, light = { fg: Color::Byte(219), bg: Color::Default }); add!("tab.focused"); add!("tab.unfocused", dark = { fg: Color::Byte(15), bg: Color::Byte(8), }, light = { fg: Color::Byte(15), bg: Color::Byte(8), }); add!("tab.bar"); add!( "widgets.list.header", dark = { fg: Color::Black, bg: Color::White, attrs: Attr::BOLD }, light = {fg: Color::White, bg: Color::Black, attrs: Attr::BOLD } ); add!( "widgets.form.label", dark = { attrs: Attr::BOLD }, light = { attrs: Attr::BOLD } ); add!("widgets.form.field"); add!("widgets.form.highlighted", light = { bg: Color::Byte(246) }, dark = { bg: Color::Byte(246) }); add!("widgets.options.highlighted", light = { bg: Color::Byte(8) }, dark = { bg: Color::Byte(8) }); /* Mail Sidebar */ add!("mail.sidebar"); add!("mail.sidebar_divider"); add!( "mail.sidebar_account_name", dark = { fg: "mail.sidebar", bg: "mail.sidebar", attrs: Attr::BOLD, }, light = { fg: "mail.sidebar", bg: "mail.sidebar", attrs: Attr::BOLD, } ); add!("mail.sidebar_unread_count", dark = { fg: Color::Byte(243) }); add!("mail.sidebar_index", dark = { fg: Color::Byte(243) }); add!("mail.sidebar_highlighted", dark = { fg: Color::Byte(233), bg: Color::Byte(15) }); add!( "mail.sidebar_highlighted_unread_count", light = { fg: "mail.sidebar_highlighted", bg: "mail.sidebar_highlighted" }, dark = { fg: "mail.sidebar_highlighted", bg: "mail.sidebar_highlighted" } ); add!( "mail.sidebar_highlighted_index", light = { fg: "mail.sidebar_index", bg: "mail.sidebar_highlighted", }, dark = { fg: "mail.sidebar_index", bg: "mail.sidebar_highlighted", }, ); add!( "mail.sidebar_highlighted_account", dark = { fg: Color::Byte(15), bg: Color::Byte(233), } ); add!( "mail.sidebar_highlighted_account_name", dark = { fg: "mail.sidebar_highlighted_account", bg: "mail.sidebar_highlighted_account", attrs: Attr::BOLD, }, light = { fg: "mail.sidebar_highlighted_account", bg: "mail.sidebar_highlighted_account", attrs: Attr::BOLD, } ); add!( "mail.sidebar_highlighted_account_unread_count", light = { fg: "mail.sidebar_unread_count", bg: "mail.sidebar_highlighted_account", }, dark = { fg: "mail.sidebar_unread_count", bg: "mail.sidebar_highlighted_account" } ); add!( "mail.sidebar_highlighted_account_index", light = { fg: "mail.sidebar_index", bg: "mail.sidebar_highlighted_account" }, dark = { fg: "mail.sidebar_index", bg: "mail.sidebar_highlighted_account" } ); /* CompactListing */ add!("mail.listing.compact.even", dark = { bg: Color::Byte(236) }, light = { bg: Color::Byte(252) } ); add!("mail.listing.compact.odd"); add!( "mail.listing.compact.even_unseen", dark = { fg: Color::Byte(0), bg: Color::Byte(251) }, light = { fg: Color::Byte(0), bg: Color::Byte(251) } ); add!( "mail.listing.compact.odd_unseen", dark = { fg: Color::Byte(0), bg: Color::Byte(251) }, light = { fg: Color::Byte(0), bg: Color::Byte(251) } ); add!("mail.listing.compact.even_selected", dark = { bg: Color::Byte(210) }, light = { bg: Color::Byte(210) } ); add!("mail.listing.compact.odd_selected", dark = { bg: Color::Byte(210) }, light = { bg: Color::Byte(210) } ); add!( "mail.listing.compact.even_highlighted", dark = { bg: Color::Byte(246) }, light = { bg: Color::Byte(244) } ); add!( "mail.listing.compact.odd_highlighted", dark = { bg: Color::Byte(246) }, light = { bg: Color::Byte(244) } ); /* ConversationsListing */ add!("mail.listing.conversations", dark = { /* Grey */ fg: Color::Byte(8), }, light = { /* Grey */ fg: Color::Byte(8), } ); add!("mail.listing.conversations.subject"); add!("mail.listing.conversations.from", dark = { /* Grey */ fg: Color::Byte(8), }, light = { /* Grey */ fg: Color::Byte(8), } ); add!("mail.listing.conversations.date", dark = { fg: Color::Magenta, }, light = { fg: Color::Magenta, } ); add!( "mail.listing.conversations.padding", dark = { fg: Color::Byte(235), bg: Color::Byte(235), }, light = { fg: Color::Byte(254), bg: Color::Byte(254), } ); add!( "mail.listing.conversations.unseen_padding", dark = { fg: Color::Byte(235), bg: Color::Byte(235), }, light = { fg: Color::Byte(254), bg: Color::Byte(254), } ); add!( "mail.listing.conversations.unseen", dark = { fg: Color::Byte(0), bg: Color::Byte(251) }, light = { fg: Color::Byte(0), bg: Color::Byte(251) } ); add!( "mail.listing.conversations.highlighted", dark = { bg: Color::Byte(246), attrs: Attr::BOLD, }, light = { bg: Color::Byte(246), attrs: Attr::BOLD, } ); add!("mail.listing.conversations.selected", dark = { bg: Color::Byte(210), }, light = { bg: Color::Byte(210) } ); /* PlainListing */ add!("mail.listing.plain.even", dark = { bg: Color::Byte(236) }, light = { bg: Color::Byte(252) } ); add!("mail.listing.plain.odd"); add!( "mail.listing.plain.even_unseen", dark = { fg: Color::Byte(0), bg: Color::Byte(251) }, light = { fg: Color::Byte(0), bg: Color::Byte(251) } ); add!( "mail.listing.plain.odd_unseen", dark = { fg: Color::Byte(0), bg: Color::Byte(251) }, light = { fg: Color::Byte(0), bg: Color::Byte(251) } ); add!("mail.listing.plain.even_selected", dark = { bg: Color::Byte(210) }, light = { bg: Color::Byte(210) } ); add!("mail.listing.plain.odd_selected", dark = { bg: Color::Byte(210) }, light = { bg: Color::Byte(210) } ); add!( "mail.listing.plain.even_highlighted", dark = { bg: Color::Byte(246) }, light = { bg: Color::Byte(244) } ); add!( "mail.listing.plain.odd_highlighted", dark = { bg: Color::Byte(246) }, light = { bg: Color::Byte(244) } ); add!( "mail.view.headers", dark = { fg: Color::Byte(33), }, light = { fg: Color::Black, } ); add!( "mail.view.headers_names", light = { fg: "mail.view.headers", bg: "mail.view.headers", attrs: "mail.view.headers", }, dark = { fg: "mail.view.headers", bg: "mail.view.headers", attrs: "mail.view.headers", } ); add!("mail.view.headers_area"); add!("mail.view.body"); add!("mail.view.thread.indentation.a", light = { bg: Color::Byte(69) }, dark = { bg: Color::Byte(69) }); // CornflowerBlue add!("mail.view.thread.indentation.b", light = { bg: Color::Byte(196) }, dark = { bg: Color::Byte(196) }); // Red1 add!("mail.view.thread.indentation.c", light = { bg: Color::Byte(175) }, dark = { bg: Color::Byte(175) }); // Pink3 add!("mail.view.thread.indentation.d", light = { bg: Color::Byte(220) }, dark = { bg: Color::Byte(220) }); // Gold1 add!("mail.view.thread.indentation.e", light = { bg: Color::Byte(172) }, dark = { bg: Color::Byte(172) }); // Orange3 add!("mail.view.thread.indentation.f", light = { bg: Color::Byte(72) }, dark = { bg: Color::Byte(72) }); // CadetBlue add!( "mail.listing.attachment_flag", light = { fg: Color::Byte(103), }, dark = { fg: Color::Byte(103) } ); add!( "mail.listing.thread_snooze_flag", light = { fg: Color::Red, }, dark = { fg: Color::Red, } ); add!( "mail.listing.tag_default", light = { fg: Color::White, bg: Color::Byte(8), attrs: Attr::BOLD }, dark = { fg: Color::White, bg: Color::Byte(8), attrs: Attr::BOLD } ); add!("pager.highlight_search", light = { fg: Color::White, bg: Color::Byte(6) /* Teal */, attrs: Attr::BOLD }, dark = { fg: Color::White, bg: Color::Byte(6) /* Teal */, attrs: Attr::BOLD }); add!("pager.highlight_search_current", light = { fg: Color::White, bg: Color::Byte(17) /* NavyBlue */, attrs: Attr::BOLD }, dark = { fg: Color::White, bg: Color::Byte(17) /* NavyBlue */, attrs: Attr::BOLD }); Themes { light: Theme { keys: light, attr_aliases: Default::default(), color_aliases: Default::default(), #[cfg(feature = "regexp")] text_format_regexps: DEFAULT_TEXT_FORMATTER_KEYS .iter() .map(|&k| (k.into(), SmallVec::new())) .collect(), }, dark: Theme { keys: dark, attr_aliases: Default::default(), color_aliases: Default::default(), #[cfg(feature = "regexp")] text_format_regexps: DEFAULT_TEXT_FORMATTER_KEYS .iter() .map(|&k| (k.into(), SmallVec::new())) .collect(), }, other_themes, } } } impl Serialize for Themes { fn serialize(&self, serializer: S) -> std::result::Result where S: Serializer, { let mut dark: IndexMap, ThemeAttribute> = Default::default(); let mut light: IndexMap, ThemeAttribute> = Default::default(); let mut other_themes: IndexMap = Default::default(); for k in self.dark.keys() { dark.insert( k.clone(), ThemeAttribute { fg: unlink_fg(&self.dark, &ColorField::Fg, k), bg: unlink_bg(&self.dark, &ColorField::Bg, k), attrs: unlink_attrs(&self.dark, k), }, ); } for k in self.light.keys() { light.insert( k.clone(), ThemeAttribute { fg: unlink_fg(&self.light, &ColorField::Fg, k), bg: unlink_bg(&self.light, &ColorField::Bg, k), attrs: unlink_attrs(&self.light, k), }, ); } for (name, t) in self.other_themes.iter() { let mut new_map: IndexMap, ThemeAttribute> = Default::default(); for k in t.keys() { new_map.insert( k.clone(), ThemeAttribute { fg: unlink_fg(&t, &ColorField::Fg, k), bg: unlink_bg(&t, &ColorField::Bg, k), attrs: unlink_attrs(&t, k), }, ); } other_themes.insert(name.to_string(), new_map); } other_themes.insert("light".to_string(), light); other_themes.insert("dark".to_string(), dark); other_themes.serialize(serializer) } } /* Check Theme linked values for cycles */ fn is_cyclic(theme: &Theme) -> std::result::Result<(), String> { #[derive(Hash, Copy, Clone, PartialEq, Eq)] enum Course { Fg, Bg, Attrs, ColorAliasFg, ColorAliasBg, AttrAlias, } fn is_cyclic_util<'a>( course: Course, k: &'a Cow<'static, str>, visited: &mut IndexMap<(&'a Cow<'static, str>, Course), bool>, stack: &mut IndexMap<(&'a Cow<'static, str>, Course), bool>, path: &mut SmallVec<[(&'a Cow<'static, str>, Course); 16]>, theme: &'a Theme, ) -> bool { if !visited[&(k, course)] { visited.entry((k, course)).and_modify(|e| *e = true); stack.entry((k, course)).and_modify(|e| *e = true); match course { Course::Fg => match theme[k].fg { ThemeValue::Link(ref l, ColorField::LikeSelf) | ThemeValue::Link(ref l, ColorField::Fg) => { path.push((l, Course::Fg)); if (!visited[&(l, Course::Fg)] && is_cyclic_util(course, l, visited, stack, path, theme)) || stack[&(l, Course::Fg)] { return true; } path.pop(); } ThemeValue::Link(ref l, ColorField::Bg) => { path.push((l, Course::Bg)); if (!visited[&(l, Course::Bg)] && is_cyclic_util(Course::Bg, l, visited, stack, path, theme)) || stack[&(l, Course::Bg)] { return true; } path.pop(); } ThemeValue::Alias(ref ident) => { path.push((ident, Course::ColorAliasFg)); if (!visited[&(ident, Course::ColorAliasFg)] && is_cyclic_util( Course::ColorAliasFg, ident, visited, stack, path, theme, )) || stack[&(ident, Course::ColorAliasFg)] { return true; } path.pop(); } _ => {} }, Course::Bg => match theme[k].bg { ThemeValue::Link(ref l, ColorField::LikeSelf) | ThemeValue::Link(ref l, ColorField::Bg) => { path.push((l, Course::Bg)); if (!visited[&(l, Course::Bg)] && is_cyclic_util(Course::Bg, l, visited, stack, path, theme)) || stack[&(l, Course::Bg)] { return true; } path.pop(); } ThemeValue::Link(ref l, ColorField::Fg) => { path.push((l, Course::Fg)); if (!visited[&(l, Course::Fg)] && is_cyclic_util(Course::Fg, l, visited, stack, path, theme)) || stack[&(l, Course::Fg)] { return true; } path.pop(); } ThemeValue::Alias(ref ident) => { path.push((ident, Course::ColorAliasBg)); if (!visited[&(ident, Course::ColorAliasBg)] && is_cyclic_util( Course::ColorAliasBg, ident, visited, stack, path, theme, )) || stack[&(ident, Course::ColorAliasBg)] { return true; } path.pop(); } _ => {} }, Course::Attrs => match theme[k].attrs { ThemeValue::Link(ref l, _) => { path.push((l, course)); if (!visited[&(l, course)] && is_cyclic_util(course, l, visited, stack, path, theme)) || stack[&(l, course)] { return true; } path.pop(); } ThemeValue::Alias(ref ident) => { path.push((ident, Course::AttrAlias)); if (!visited[&(ident, Course::AttrAlias)] && is_cyclic_util( Course::AttrAlias, ident, visited, stack, path, theme, )) || stack[&(ident, Course::AttrAlias)] { return true; } path.pop(); } _ => {} }, Course::ColorAliasFg | Course::ColorAliasBg => match &theme.color_aliases[k] { ThemeValue::Link(ref l, ref field) => { let course = match (course, field) { (Course::ColorAliasFg, ColorField::LikeSelf) => Course::Fg, (Course::ColorAliasBg, ColorField::LikeSelf) => Course::Bg, (_, ColorField::LikeSelf) => unsafe { std::hint::unreachable_unchecked() }, (_, ColorField::Fg) => Course::Fg, (_, ColorField::Bg) => Course::Bg, }; path.push((l, course)); if (!visited[&(l, course)] && is_cyclic_util(course, l, visited, stack, path, theme)) || stack[&(l, course)] { return true; } path.pop(); } ThemeValue::Alias(ref ident) => { path.push((ident, course)); if (!visited[&(ident, course)] && is_cyclic_util(course, ident, visited, stack, path, theme)) || stack[&(ident, course)] { return true; } path.pop(); } _ => {} }, Course::AttrAlias => match &theme.attr_aliases[k] { ThemeValue::Link(ref l, ()) => { path.push((l, Course::Attrs)); if (!visited[&(l, Course::Attrs)] && is_cyclic_util(Course::Attrs, l, visited, stack, path, theme)) || stack[&(l, Course::Attrs)] { return true; } path.pop(); } ThemeValue::Alias(ref ident) => { path.push((ident, course)); if (!visited[&(ident, course)] && is_cyclic_util(course, ident, visited, stack, path, theme)) || stack[&(ident, course)] { return true; } path.pop(); } _ => {} }, } } stack.entry((k, course)).and_modify(|e| *e = false); false } let mut path = SmallVec::new(); let mut visited = theme .keys() .map(|k| { std::iter::once(((k, Course::Fg), false)) .chain(std::iter::once(((k, Course::Bg), false))) .chain(std::iter::once(((k, Course::Attrs), false))) }) .flatten() .chain( theme .color_aliases .keys() .map(|k| { std::iter::once(((k, Course::ColorAliasFg), false)) .chain(std::iter::once(((k, Course::ColorAliasBg), false))) }) .flatten(), ) .chain( theme .attr_aliases .keys() .map(|k| ((k, Course::AttrAlias), false)), ) .collect::, Course), bool>>(); let mut stack = visited.clone(); for k in theme.keys() { for &course in [Course::Fg, Course::Bg, Course::Attrs].iter() { path.push((k, course)); if is_cyclic_util(course, k, &mut visited, &mut stack, &mut path, &theme) { let path = path .into_iter() .map(|(k, c)| match c { Course::Fg => format!("{}.fg", k,), Course::Bg => format!("{}.fg", k,), Course::Attrs => format!("{}.attrs", k,), Course::ColorAliasFg => format!("(Color fg) ${}", k), Course::ColorAliasBg => format!("(Color bg) ${}", k), Course::AttrAlias => format!("(Attr) ${}", k), }) .collect::>(); return Err(format!( "{} {}", match course { Course::Fg => "fg: ", Course::Bg => "bg: ", Course::Attrs => "attrs: ", Course::ColorAliasFg => "color alias fg: ", Course::ColorAliasBg => "color alias bg: ", Course::AttrAlias => "attribute alias: ", }, path.join(" -> ") )); } for v in visited.values_mut() { *v = false; } path.pop(); } } return Ok(()); } #[test] fn test_theme_parsing() { /* MUST SUCCEED: default themes should be valid */ let def = Themes::default(); assert!(def.validate().is_ok()); /* MUST SUCCEED: new user theme `hunter2`, theme `dark` has user redefinitions */ const TEST_STR: &str = r##"[dark] "mail.listing.tag_default" = { fg = "White", bg = "HotPink3" } "mail.listing.attachment_flag" = { fg = "mail.listing.tag_default.bg" } "mail.view.headers" = { bg = "mail.listing.tag_default.fg" } ["hunter2"] "mail.view.body" = { fg = "Black", bg = "White"}"##; let parsed: Themes = toml::from_str(TEST_STR).unwrap(); assert!(parsed.other_themes.contains_key("hunter2")); assert_eq!( unlink_bg( &parsed.dark, &ColorField::Bg, &Cow::from("mail.listing.tag_default") ), Color::Byte(132) ); assert_eq!( unlink_fg( &parsed.dark, &ColorField::Fg, &Cow::from("mail.listing.attachment_flag") ), Color::Byte(132) ); assert_eq!( unlink_bg( &parsed.dark, &ColorField::Bg, &Cow::from("mail.view.headers") ), Color::Byte(15), // White ); assert!(parsed.validate().is_ok()); /* MUST FAIL: theme `dark` contains a cycle */ const HAS_CYCLE: &str = r##"[dark] "mail.listing.compact.even" = { fg = "mail.listing.compact.odd" } "mail.listing.compact.odd" = { fg = "mail.listing.compact.even" } "##; let parsed: Themes = toml::from_str(HAS_CYCLE).unwrap(); assert!(parsed.validate().is_err()); /* MUST FAIL: theme `dark` contains an invalid key */ const HAS_INVALID_KEYS: &str = r##"[dark] "asdfsafsa" = { fg = "Black" } "##; let parsed: std::result::Result = toml::from_str(HAS_INVALID_KEYS); assert!(parsed.is_err()); /* MUST SUCCEED: alias $Jebediah resolves to a valid color */ const TEST_ALIAS_STR: &str = r##"[dark] color_aliases= { "Jebediah" = "#b4da55" } "mail.listing.tag_default" = { fg = "$Jebediah" } "##; let parsed: Themes = toml::from_str(TEST_ALIAS_STR).unwrap(); assert!(parsed.validate().is_ok()); assert_eq!( unlink_fg( &parsed.dark, &ColorField::Fg, &Cow::from("mail.listing.tag_default") ), Color::Rgb(180, 218, 85) ); /* MUST FAIL: Mispell color alias $Jebediah as $Jebedia */ const TEST_INVALID_ALIAS_STR: &str = r##"[dark] color_aliases= { "Jebediah" = "#b4da55" } "mail.listing.tag_default" = { fg = "$Jebedia" } "##; let parsed: Themes = toml::from_str(TEST_INVALID_ALIAS_STR).unwrap(); assert!(parsed.validate().is_err()); /* MUST FAIL: Color alias $Jebediah is defined as itself */ const TEST_CYCLIC_ALIAS_STR: &str = r##"[dark] color_aliases= { "Jebediah" = "$Jebediah" } "mail.listing.tag_default" = { fg = "$Jebediah" } "##; let parsed: Themes = toml::from_str(TEST_CYCLIC_ALIAS_STR).unwrap(); assert!(parsed.validate().is_err()); /* MUST FAIL: Attr alias $Jebediah is defined as itself */ const TEST_CYCLIC_ALIAS_ATTR_STR: &str = r##"[dark] attr_aliases= { "Jebediah" = "$Jebediah" } "mail.listing.tag_default" = { attrs = "$Jebediah" } "##; let parsed: Themes = toml::from_str(TEST_CYCLIC_ALIAS_ATTR_STR).unwrap(); assert!(parsed.validate().is_err()); /* MUST FAIL: alias $Jebediah resolves to a cycle */ const TEST_CYCLIC_ALIAS_STR_2: &str = r##"[dark] color_aliases= { "Jebediah" = "$JebediahJr", "JebediahJr" = "mail.listing.tag_default" } "mail.listing.tag_default" = { fg = "$Jebediah" } "##; let parsed: Themes = toml::from_str(TEST_CYCLIC_ALIAS_STR_2).unwrap(); assert!(parsed.validate().is_err()); /* MUST SUCCEED: alias $Jebediah resolves to a key's field */ const TEST_CYCLIC_ALIAS_STR_3: &str = r##"[dark] color_aliases= { "Jebediah" = "$JebediahJr", "JebediahJr" = "mail.listing.tag_default.bg" } "mail.listing.tag_default" = { fg = "$Jebediah", bg = "Black" } "##; let parsed: Themes = toml::from_str(TEST_CYCLIC_ALIAS_STR_3).unwrap(); assert!(parsed.validate().is_ok()); /* MUST FAIL: alias $Jebediah resolves to an invalid key */ const TEST_INVALID_LINK_KEY_FIELD_STR: &str = r##"[dark] color_aliases= { "Jebediah" = "$JebediahJr", "JebediahJr" = "mail.listing.tag_default.attrs" } "mail.listing.tag_default" = { fg = "$Jebediah", bg = "Black" } "##; let parsed: Themes = toml::from_str(TEST_INVALID_LINK_KEY_FIELD_STR).unwrap(); assert!(parsed.validate().is_err()); }