diff --git a/src/conf/themes.rs b/src/conf/themes.rs index 025c3b18..5651908b 100644 --- a/src/conf/themes.rs +++ b/src/conf/themes.rs @@ -122,6 +122,21 @@ fn unlink_fg<'k, 't: 'k>( 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 { @@ -129,6 +144,20 @@ fn unlink_fg<'k, 't: 'k>( 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, }, } @@ -148,6 +177,20 @@ fn unlink_bg<'k, 't: 'k>( 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 { @@ -155,6 +198,20 @@ fn unlink_bg<'k, 't: 'k>( 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, }, } @@ -166,6 +223,19 @@ fn unlink_attrs<'k, 't: 'k>(theme: &'t Theme, mut key: &'k Cow<'static, str>) -> 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, } } @@ -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. enum ThemeValue { Value(T), + Alias(Cow<'static, str>), Link(Cow<'static, str>, T::LinkType), } @@ -333,7 +404,9 @@ impl<'de> Deserialize<'de> for ThemeValue { D: Deserializer<'de>, { if let Ok(s) = ::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)) } else { Ok(ThemeValue::Link(s.into(), ())) @@ -351,6 +424,7 @@ impl Serialize for ThemeValue { { match self { ThemeValue::Value(s) => s.serialize(serializer), + ThemeValue::Alias(s) => format!("${}", s).serialize(serializer), ThemeValue::Link(s, ()) => serializer.serialize_str(s.as_ref()), } } @@ -363,6 +437,7 @@ impl Serialize for ThemeValue { { 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()) @@ -380,7 +455,9 @@ impl<'de> Deserialize<'de> for ThemeValue { D: Deserializer<'de>, { if let Ok(s) = ::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)) } else if s.ends_with(".fg") { Ok(ThemeValue::Link( @@ -410,6 +487,8 @@ pub struct Themes { #[derive(Debug, Clone)] pub struct Theme { + color_aliases: HashMap, ThemeValue>, + attr_aliases: HashMap, ThemeValue>, pub keys: HashMap, ThemeAttributeInner>, } @@ -443,6 +522,10 @@ impl<'de> Deserialize<'de> for Themes { } #[derive(Deserialize, Default)] struct ThemeOptions { + #[serde(default)] + color_aliases: HashMap, ThemeValue>, + #[serde(default)] + attr_aliases: HashMap, ThemeValue>, #[serde(flatten, default)] keys: HashMap, ThemeAttributeInnerOptions>, } @@ -487,6 +570,8 @@ impl<'de> Deserialize<'de> for Themes { .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() { if let Some(mut att) = s.dark.keys.remove(k) { if let Some(att) = att.fg.take() { @@ -512,13 +597,12 @@ impl<'de> Deserialize<'de> for Themes { .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() { + let mut theme = s.other_themes.remove(tk).unwrap(); for (k, v) in t.iter_mut() { - if let Some(mut att) = s - .other_themes - .get_mut(tk) - .and_then(|theme| theme.keys.remove(k)) - { + if let Some(mut att) = theme.keys.remove(k) { if let Some(att) = att.fg.take() { 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!( "{} theme contains unrecognized theme keywords: {}", tk, - s.other_themes[tk] + theme .keys .keys() .into_iter() @@ -543,6 +627,8 @@ impl<'de> Deserialize<'de> for Themes { .join(", ") ))); } + t.color_aliases = theme.color_aliases; + t.attr_aliases = theme.attr_aliases; } Ok(ret) } @@ -554,15 +640,64 @@ impl Themes { .keys() .filter_map(|k| { if !hash_set.contains(&k.as_ref()) { - Some((None, "key", 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", 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 } @@ -573,7 +708,13 @@ impl Themes { .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", 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 } @@ -584,7 +725,18 @@ impl Themes { .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", 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 } @@ -592,17 +744,17 @@ impl Themes { None } })) - .collect::, &'_ str, &'_ str); 128]>>(); + .collect::, &'_ str, &'_ str, &'_ str); 128]>>(); if !keys.is_empty() { return Err(format!( - "{} theme contains unrecognized theme keywords: {}", + "{} theme contains invalid data: {}", name, keys.into_iter() - .map(|(key_opt, desc, link)| if let Some(key) = key_opt { - format!("{} {}: \"{}\"", key, desc, link) + .map(|(key_opt, desc, kind, link)| if let Some(key) = key_opt { + format!("{} {}: {} \"{}\"", key, desc, kind, link) } else { - format!("{}: \"{}\"", desc, link) + format!("{}: {} \"{}\"", desc, kind, link) }) .collect::>() .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_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 }, - dark: Theme { keys: dark }, + light: Theme { + keys: light, + attr_aliases: Default::default(), + color_aliases: Default::default(), + }, + dark: Theme { + keys: dark, + attr_aliases: Default::default(), + color_aliases: Default::default(), + }, other_themes, } } @@ -1103,6 +1263,9 @@ fn is_cyclic(theme: &Theme) -> std::result::Result<(), String> { Fg, Bg, Attrs, + ColorAliasFg, + ColorAliasBg, + AttrAlias, } fn is_cyclic_util<'a>( course: Course, @@ -1141,6 +1304,24 @@ fn is_cyclic(theme: &Theme) -> std::result::Result<(), String> { } 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 { @@ -1167,6 +1348,24 @@ fn is_cyclic(theme: &Theme) -> std::result::Result<(), String> { } 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 { @@ -1181,6 +1380,76 @@ fn is_cyclic(theme: &Theme) -> std::result::Result<(), String> { } 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))) }) .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(); @@ -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) { let path = path .into_iter() - .map(|(k, c)| { - format!( - "{}.{}", - k, - match c { - Course::Fg => "fg", - Course::Bg => "bg", - Course::Attrs => "attrs", - } - ) + .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!( @@ -1225,6 +1507,9 @@ fn is_cyclic(theme: &Theme) -> std::result::Result<(), String> { 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(" -> ") )); @@ -1241,8 +1526,10 @@ fn is_cyclic(theme: &Theme) -> std::result::Result<(), String> { #[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: &'static str = r##"[dark] "mail.listing.tag_default" = { fg = "White", bg = "HotPink3" } "mail.listing.attachment_flag" = { fg = "mail.listing.tag_default.bg" } @@ -1277,15 +1564,74 @@ fn test_theme_parsing() { Color::Byte(15), // White ); assert!(parsed.validate().is_ok()); + /* MUST FAIL: theme `dark` contains a cycle */ const HAS_CYCLE: &'static 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: &'static 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: &'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()); }