themes: Add support for Color/Attribute aliases
Add aliases to avoid repetition of raw values when defining new themes. Aliases are strings starting with "$" and must be defined in the `color_aliases` and `attr_aliases` fields of a theme.memfd
parent
eca8a30c3f
commit
de03b106f3
|
@ -122,6 +122,21 @@ fn unlink_fg<'k, 't: 'k>(
|
||||||
key = new_key;
|
key = new_key;
|
||||||
field = new_field
|
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,
|
ThemeValue::Value(val) => return *val,
|
||||||
},
|
},
|
||||||
ColorField::Bg => match &theme[key].bg {
|
ColorField::Bg => match &theme[key].bg {
|
||||||
|
@ -129,6 +144,20 @@ fn unlink_fg<'k, 't: 'k>(
|
||||||
key = new_key;
|
key = new_key;
|
||||||
field = new_field
|
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,
|
ThemeValue::Value(val) => return *val,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -148,6 +177,20 @@ fn unlink_bg<'k, 't: 'k>(
|
||||||
key = new_key;
|
key = new_key;
|
||||||
field = new_field
|
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,
|
ThemeValue::Value(val) => return *val,
|
||||||
},
|
},
|
||||||
ColorField::Fg => match &theme[key].fg {
|
ColorField::Fg => match &theme[key].fg {
|
||||||
|
@ -155,6 +198,20 @@ fn unlink_bg<'k, 't: 'k>(
|
||||||
key = new_key;
|
key = new_key;
|
||||||
field = new_field
|
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,
|
ThemeValue::Value(val) => return *val,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -166,6 +223,19 @@ fn unlink_attrs<'k, 't: 'k>(theme: &'t Theme, mut key: &'k Cow<'static, str>) ->
|
||||||
loop {
|
loop {
|
||||||
match &theme[key].attrs {
|
match &theme[key].attrs {
|
||||||
ThemeValue::Link(ref new_key, ()) => key = new_key,
|
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,
|
ThemeValue::Value(val) => return *val,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -288,6 +358,7 @@ impl ThemeLink for Attr {
|
||||||
/// Holds either an actual value or refers to the key name of the attribute that holds the value.
|
/// Holds either an actual value or refers to the key name of the attribute that holds the value.
|
||||||
enum ThemeValue<T: ThemeLink> {
|
enum ThemeValue<T: ThemeLink> {
|
||||||
Value(T),
|
Value(T),
|
||||||
|
Alias(Cow<'static, str>),
|
||||||
Link(Cow<'static, str>, T::LinkType),
|
Link(Cow<'static, str>, T::LinkType),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -333,7 +404,9 @@ impl<'de> Deserialize<'de> for ThemeValue<Attr> {
|
||||||
D: Deserializer<'de>,
|
D: Deserializer<'de>,
|
||||||
{
|
{
|
||||||
if let Ok(s) = <String>::deserialize(deserializer) {
|
if let Ok(s) = <String>::deserialize(deserializer) {
|
||||||
if let Ok(c) = Attr::from_string_de::<'de, D, String>(s.clone()) {
|
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))
|
Ok(ThemeValue::Value(c))
|
||||||
} else {
|
} else {
|
||||||
Ok(ThemeValue::Link(s.into(), ()))
|
Ok(ThemeValue::Link(s.into(), ()))
|
||||||
|
@ -351,6 +424,7 @@ impl Serialize for ThemeValue<Attr> {
|
||||||
{
|
{
|
||||||
match self {
|
match self {
|
||||||
ThemeValue::Value(s) => s.serialize(serializer),
|
ThemeValue::Value(s) => s.serialize(serializer),
|
||||||
|
ThemeValue::Alias(s) => format!("${}", s).serialize(serializer),
|
||||||
ThemeValue::Link(s, ()) => serializer.serialize_str(s.as_ref()),
|
ThemeValue::Link(s, ()) => serializer.serialize_str(s.as_ref()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -363,6 +437,7 @@ impl Serialize for ThemeValue<Color> {
|
||||||
{
|
{
|
||||||
match self {
|
match self {
|
||||||
ThemeValue::Value(s) => s.serialize(serializer),
|
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::LikeSelf) => serializer.serialize_str(s.as_ref()),
|
||||||
ThemeValue::Link(s, ColorField::Fg) => {
|
ThemeValue::Link(s, ColorField::Fg) => {
|
||||||
serializer.serialize_str(format!("{}.fg", s).as_ref())
|
serializer.serialize_str(format!("{}.fg", s).as_ref())
|
||||||
|
@ -380,7 +455,9 @@ impl<'de> Deserialize<'de> for ThemeValue<Color> {
|
||||||
D: Deserializer<'de>,
|
D: Deserializer<'de>,
|
||||||
{
|
{
|
||||||
if let Ok(s) = <String>::deserialize(deserializer) {
|
if let Ok(s) = <String>::deserialize(deserializer) {
|
||||||
if let Ok(c) = Color::from_string_de::<'de, D>(s.clone()) {
|
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))
|
Ok(ThemeValue::Value(c))
|
||||||
} else if s.ends_with(".fg") {
|
} else if s.ends_with(".fg") {
|
||||||
Ok(ThemeValue::Link(
|
Ok(ThemeValue::Link(
|
||||||
|
@ -410,6 +487,8 @@ pub struct Themes {
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Theme {
|
pub struct Theme {
|
||||||
|
color_aliases: HashMap<Cow<'static, str>, ThemeValue<Color>>,
|
||||||
|
attr_aliases: HashMap<Cow<'static, str>, ThemeValue<Attr>>,
|
||||||
pub keys: HashMap<Cow<'static, str>, ThemeAttributeInner>,
|
pub keys: HashMap<Cow<'static, str>, ThemeAttributeInner>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -443,6 +522,10 @@ impl<'de> Deserialize<'de> for Themes {
|
||||||
}
|
}
|
||||||
#[derive(Deserialize, Default)]
|
#[derive(Deserialize, Default)]
|
||||||
struct ThemeOptions {
|
struct ThemeOptions {
|
||||||
|
#[serde(default)]
|
||||||
|
color_aliases: HashMap<Cow<'static, str>, ThemeValue<Color>>,
|
||||||
|
#[serde(default)]
|
||||||
|
attr_aliases: HashMap<Cow<'static, str>, ThemeValue<Attr>>,
|
||||||
#[serde(flatten, default)]
|
#[serde(flatten, default)]
|
||||||
keys: HashMap<Cow<'static, str>, ThemeAttributeInnerOptions>,
|
keys: HashMap<Cow<'static, str>, ThemeAttributeInnerOptions>,
|
||||||
}
|
}
|
||||||
|
@ -487,6 +570,8 @@ impl<'de> Deserialize<'de> for Themes {
|
||||||
.join(", ")
|
.join(", ")
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
ret.light.color_aliases = s.light.color_aliases;
|
||||||
|
ret.light.attr_aliases = s.light.attr_aliases;
|
||||||
for (k, v) in ret.dark.iter_mut() {
|
for (k, v) in ret.dark.iter_mut() {
|
||||||
if let Some(mut att) = s.dark.keys.remove(k) {
|
if let Some(mut att) = s.dark.keys.remove(k) {
|
||||||
if let Some(att) = att.fg.take() {
|
if let Some(att) = att.fg.take() {
|
||||||
|
@ -512,13 +597,12 @@ impl<'de> Deserialize<'de> for Themes {
|
||||||
.join(", ")
|
.join(", ")
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
ret.dark.color_aliases = s.dark.color_aliases;
|
||||||
|
ret.dark.attr_aliases = s.dark.attr_aliases;
|
||||||
for (tk, t) in ret.other_themes.iter_mut() {
|
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() {
|
for (k, v) in t.iter_mut() {
|
||||||
if let Some(mut att) = s
|
if let Some(mut att) = theme.keys.remove(k) {
|
||||||
.other_themes
|
|
||||||
.get_mut(tk)
|
|
||||||
.and_then(|theme| theme.keys.remove(k))
|
|
||||||
{
|
|
||||||
if let Some(att) = att.fg.take() {
|
if let Some(att) = att.fg.take() {
|
||||||
v.fg = att;
|
v.fg = att;
|
||||||
}
|
}
|
||||||
|
@ -530,11 +614,11 @@ impl<'de> Deserialize<'de> for Themes {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !s.other_themes[tk].keys.is_empty() {
|
if !theme.keys.is_empty() {
|
||||||
return Err(de::Error::custom(format!(
|
return Err(de::Error::custom(format!(
|
||||||
"{} theme contains unrecognized theme keywords: {}",
|
"{} theme contains unrecognized theme keywords: {}",
|
||||||
tk,
|
tk,
|
||||||
s.other_themes[tk]
|
theme
|
||||||
.keys
|
.keys
|
||||||
.keys()
|
.keys()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -543,6 +627,8 @@ impl<'de> Deserialize<'de> for Themes {
|
||||||
.join(", ")
|
.join(", ")
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
t.color_aliases = theme.color_aliases;
|
||||||
|
t.attr_aliases = theme.attr_aliases;
|
||||||
}
|
}
|
||||||
Ok(ret)
|
Ok(ret)
|
||||||
}
|
}
|
||||||
|
@ -554,15 +640,64 @@ impl Themes {
|
||||||
.keys()
|
.keys()
|
||||||
.filter_map(|k| {
|
.filter_map(|k| {
|
||||||
if !hash_set.contains(&k.as_ref()) {
|
if !hash_set.contains(&k.as_ref()) {
|
||||||
Some((None, "key", k.as_ref()))
|
Some((None, "key", "invalid key", k.as_ref()))
|
||||||
} else {
|
} else {
|
||||||
None
|
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)| {
|
.chain(theme.iter().filter_map(|(key, a)| {
|
||||||
if let ThemeValue::Link(ref r, _) = a.fg {
|
if let ThemeValue::Link(ref r, _) = a.fg {
|
||||||
if !hash_set.contains(&r.as_ref()) {
|
if !hash_set.contains(&r.as_ref()) {
|
||||||
Some((Some(key), "fg link", 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 {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -573,7 +708,13 @@ impl Themes {
|
||||||
.chain(theme.iter().filter_map(|(key, a)| {
|
.chain(theme.iter().filter_map(|(key, a)| {
|
||||||
if let ThemeValue::Link(ref r, _) = a.bg {
|
if let ThemeValue::Link(ref r, _) = a.bg {
|
||||||
if !hash_set.contains(&r.as_ref()) {
|
if !hash_set.contains(&r.as_ref()) {
|
||||||
Some((Some(key), "bg link", 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 {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -584,7 +725,18 @@ impl Themes {
|
||||||
.chain(theme.iter().filter_map(|(key, a)| {
|
.chain(theme.iter().filter_map(|(key, a)| {
|
||||||
if let ThemeValue::Link(ref r, _) = a.attrs {
|
if let ThemeValue::Link(ref r, _) = a.attrs {
|
||||||
if !hash_set.contains(&r.as_ref()) {
|
if !hash_set.contains(&r.as_ref()) {
|
||||||
Some((Some(key), "attrs link", 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 {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -592,17 +744,17 @@ impl Themes {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.collect::<SmallVec<[(Option<_>, &'_ str, &'_ str); 128]>>();
|
.collect::<SmallVec<[(Option<_>, &'_ str, &'_ str, &'_ str); 128]>>();
|
||||||
|
|
||||||
if !keys.is_empty() {
|
if !keys.is_empty() {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"{} theme contains unrecognized theme keywords: {}",
|
"{} theme contains invalid data: {}",
|
||||||
name,
|
name,
|
||||||
keys.into_iter()
|
keys.into_iter()
|
||||||
.map(|(key_opt, desc, link)| if let Some(key) = key_opt {
|
.map(|(key_opt, desc, kind, link)| if let Some(key) = key_opt {
|
||||||
format!("{} {}: \"{}\"", key, desc, link)
|
format!("{} {}: {} \"{}\"", key, desc, kind, link)
|
||||||
} else {
|
} else {
|
||||||
format!("{}: \"{}\"", desc, link)
|
format!("{}: {} \"{}\"", desc, kind, link)
|
||||||
})
|
})
|
||||||
.collect::<SmallVec<[String; 128]>>()
|
.collect::<SmallVec<[String; 128]>>()
|
||||||
.join(", ")
|
.join(", ")
|
||||||
|
@ -1036,8 +1188,16 @@ impl Default for Themes {
|
||||||
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", 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 });
|
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 {
|
Themes {
|
||||||
light: Theme { keys: light },
|
light: Theme {
|
||||||
dark: Theme { keys: dark },
|
keys: light,
|
||||||
|
attr_aliases: Default::default(),
|
||||||
|
color_aliases: Default::default(),
|
||||||
|
},
|
||||||
|
dark: Theme {
|
||||||
|
keys: dark,
|
||||||
|
attr_aliases: Default::default(),
|
||||||
|
color_aliases: Default::default(),
|
||||||
|
},
|
||||||
other_themes,
|
other_themes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1103,6 +1263,9 @@ fn is_cyclic(theme: &Theme) -> std::result::Result<(), String> {
|
||||||
Fg,
|
Fg,
|
||||||
Bg,
|
Bg,
|
||||||
Attrs,
|
Attrs,
|
||||||
|
ColorAliasFg,
|
||||||
|
ColorAliasBg,
|
||||||
|
AttrAlias,
|
||||||
}
|
}
|
||||||
fn is_cyclic_util<'a>(
|
fn is_cyclic_util<'a>(
|
||||||
course: Course,
|
course: Course,
|
||||||
|
@ -1141,6 +1304,24 @@ fn is_cyclic(theme: &Theme) -> std::result::Result<(), String> {
|
||||||
}
|
}
|
||||||
path.pop();
|
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,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
} else if stack[&(ident, Course::ColorAliasFg)] {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
path.pop();
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
Course::Bg => match theme[k].bg {
|
Course::Bg => match theme[k].bg {
|
||||||
|
@ -1167,6 +1348,24 @@ fn is_cyclic(theme: &Theme) -> std::result::Result<(), String> {
|
||||||
}
|
}
|
||||||
path.pop();
|
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,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
} else if stack[&(ident, Course::ColorAliasBg)] {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
path.pop();
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
Course::Attrs => match theme[k].attrs {
|
Course::Attrs => match theme[k].attrs {
|
||||||
|
@ -1181,6 +1380,76 @@ fn is_cyclic(theme: &Theme) -> std::result::Result<(), String> {
|
||||||
}
|
}
|
||||||
path.pop();
|
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)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
} else if 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)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
} else if 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)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
} else if 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)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
} else if 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)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
} else if stack[&(ident, course)] {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
path.pop();
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -1198,6 +1467,22 @@ fn is_cyclic(theme: &Theme) -> std::result::Result<(), String> {
|
||||||
.chain(std::iter::once(((k, Course::Attrs), false)))
|
.chain(std::iter::once(((k, Course::Attrs), false)))
|
||||||
})
|
})
|
||||||
.flatten()
|
.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::<HashMap<(&Cow<'static, str>, Course), bool>>();
|
.collect::<HashMap<(&Cow<'static, str>, Course), bool>>();
|
||||||
|
|
||||||
let mut stack = visited.clone();
|
let mut stack = visited.clone();
|
||||||
|
@ -1207,16 +1492,13 @@ fn is_cyclic(theme: &Theme) -> std::result::Result<(), String> {
|
||||||
if is_cyclic_util(course, k, &mut visited, &mut stack, &mut path, &theme) {
|
if is_cyclic_util(course, k, &mut visited, &mut stack, &mut path, &theme) {
|
||||||
let path = path
|
let path = path
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(k, c)| {
|
.map(|(k, c)| match c {
|
||||||
format!(
|
Course::Fg => format!("{}.fg", k,),
|
||||||
"{}.{}",
|
Course::Bg => format!("{}.fg", k,),
|
||||||
k,
|
Course::Attrs => format!("{}.attrs", k,),
|
||||||
match c {
|
Course::ColorAliasFg => format!("(Color fg) ${}", k),
|
||||||
Course::Fg => "fg",
|
Course::ColorAliasBg => format!("(Color bg) ${}", k),
|
||||||
Course::Bg => "bg",
|
Course::AttrAlias => format!("(Attr) ${}", k),
|
||||||
Course::Attrs => "attrs",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
.collect::<Vec<String>>();
|
.collect::<Vec<String>>();
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
|
@ -1225,6 +1507,9 @@ fn is_cyclic(theme: &Theme) -> std::result::Result<(), String> {
|
||||||
Course::Fg => "fg: ",
|
Course::Fg => "fg: ",
|
||||||
Course::Bg => "bg: ",
|
Course::Bg => "bg: ",
|
||||||
Course::Attrs => "attrs: ",
|
Course::Attrs => "attrs: ",
|
||||||
|
Course::ColorAliasFg => "color alias fg: ",
|
||||||
|
Course::ColorAliasBg => "color alias bg: ",
|
||||||
|
Course::AttrAlias => "attribute alias: ",
|
||||||
},
|
},
|
||||||
path.join(" -> ")
|
path.join(" -> ")
|
||||||
));
|
));
|
||||||
|
@ -1241,8 +1526,10 @@ fn is_cyclic(theme: &Theme) -> std::result::Result<(), String> {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_theme_parsing() {
|
fn test_theme_parsing() {
|
||||||
|
/* MUST SUCCEED: default themes should be valid */
|
||||||
let def = Themes::default();
|
let def = Themes::default();
|
||||||
assert!(def.validate().is_ok());
|
assert!(def.validate().is_ok());
|
||||||
|
/* MUST SUCCEED: new user theme `hunter2`, theme `dark` has user redefinitions */
|
||||||
const TEST_STR: &'static str = r##"[dark]
|
const TEST_STR: &'static str = r##"[dark]
|
||||||
"mail.listing.tag_default" = { fg = "White", bg = "HotPink3" }
|
"mail.listing.tag_default" = { fg = "White", bg = "HotPink3" }
|
||||||
"mail.listing.attachment_flag" = { fg = "mail.listing.tag_default.bg" }
|
"mail.listing.attachment_flag" = { fg = "mail.listing.tag_default.bg" }
|
||||||
|
@ -1277,15 +1564,74 @@ fn test_theme_parsing() {
|
||||||
Color::Byte(15), // White
|
Color::Byte(15), // White
|
||||||
);
|
);
|
||||||
assert!(parsed.validate().is_ok());
|
assert!(parsed.validate().is_ok());
|
||||||
|
/* MUST FAIL: theme `dark` contains a cycle */
|
||||||
const HAS_CYCLE: &'static str = r##"[dark]
|
const HAS_CYCLE: &'static str = r##"[dark]
|
||||||
"mail.listing.compact.even" = { fg = "mail.listing.compact.odd" }
|
"mail.listing.compact.even" = { fg = "mail.listing.compact.odd" }
|
||||||
"mail.listing.compact.odd" = { fg = "mail.listing.compact.even" }
|
"mail.listing.compact.odd" = { fg = "mail.listing.compact.even" }
|
||||||
"##;
|
"##;
|
||||||
let parsed: Themes = toml::from_str(HAS_CYCLE).unwrap();
|
let parsed: Themes = toml::from_str(HAS_CYCLE).unwrap();
|
||||||
assert!(parsed.validate().is_err());
|
assert!(parsed.validate().is_err());
|
||||||
|
/* MUST FAIL: theme `dark` contains an invalid key */
|
||||||
const HAS_INVALID_KEYS: &'static str = r##"[dark]
|
const HAS_INVALID_KEYS: &'static str = r##"[dark]
|
||||||
"asdfsafsa" = { fg = "Black" }
|
"asdfsafsa" = { fg = "Black" }
|
||||||
"##;
|
"##;
|
||||||
let parsed: std::result::Result<Themes, _> = toml::from_str(HAS_INVALID_KEYS);
|
let parsed: std::result::Result<Themes, _> = toml::from_str(HAS_INVALID_KEYS);
|
||||||
assert!(parsed.is_err());
|
assert!(parsed.is_err());
|
||||||
|
/* MUST SUCCEED: alias $Jebediah resolves to a valid color */
|
||||||
|
const TEST_ALIAS_STR: &'static 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: &'static 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: &'static 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: &'static 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: &'static 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: &'static 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: &'static 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());
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue