866 lines
26 KiB
Rust
866 lines
26 KiB
Rust
/*
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
//! Application themes.
|
|
//!
|
|
//! * An attribute is a triple of foreground color, background color and terminal attribute `ThemeValue`s.
|
|
//! * A `ThemeValue<T>` 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 melib::{MeliError, Result};
|
|
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
|
use smallvec::SmallVec;
|
|
use std::borrow::Cow;
|
|
use std::collections::{HashMap, 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, &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, &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 HashMap<Cow<'static, str>, ThemeAttributeInner>,
|
|
key: &'k Cow<'static, str>,
|
|
) -> ThemeAttribute {
|
|
ThemeAttribute {
|
|
fg: unlink_fg(theme, key),
|
|
bg: unlink_bg(theme, key),
|
|
attrs: unlink_attrs(theme, key),
|
|
}
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn unlink_fg<'k, 't: 'k>(
|
|
theme: &'t HashMap<Cow<'static, str>, ThemeAttributeInner>,
|
|
mut key: &'k Cow<'static, str>,
|
|
) -> Color {
|
|
loop {
|
|
match &theme[key].fg {
|
|
ThemeValue::Link(ref new_key) => key = new_key,
|
|
ThemeValue::Value(val) => return *val,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn unlink_bg<'k, 't: 'k>(
|
|
theme: &'t HashMap<Cow<'static, str>, ThemeAttributeInner>,
|
|
mut key: &'k Cow<'static, str>,
|
|
) -> Color {
|
|
loop {
|
|
match &theme[key].bg {
|
|
ThemeValue::Link(ref new_key) => key = new_key,
|
|
ThemeValue::Value(val) => return *val,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn unlink_attrs<'k, 't: 'k>(
|
|
theme: &'t HashMap<Cow<'static, str>, ThemeAttributeInner>,
|
|
mut key: &'k Cow<'static, str>,
|
|
) -> Attr {
|
|
loop {
|
|
match &theme[key].attrs {
|
|
ThemeValue::Link(ref new_key) => key = new_key,
|
|
ThemeValue::Value(val) => return *val,
|
|
}
|
|
}
|
|
}
|
|
|
|
const DEFAULT_KEYS: &'static [&'static str] = &[
|
|
"theme_default",
|
|
"status.bar",
|
|
"status.notification",
|
|
"tab.focused",
|
|
"tab.unfocused",
|
|
"tab.bar",
|
|
"widgets.form.label",
|
|
"widgets.form.field",
|
|
"widgets.form.highlighted",
|
|
"widgets.options.highlighted",
|
|
"mail.sidebar",
|
|
"mail.sidebar_unread_count",
|
|
"mail.sidebar_index",
|
|
"mail.sidebar_highlighted",
|
|
"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.unseen",
|
|
"mail.listing.compact.selected",
|
|
"mail.listing.compact.highlighted",
|
|
"mail.listing.plain.even",
|
|
"mail.listing.plain.odd",
|
|
"mail.listing.plain.unseen",
|
|
"mail.listing.plain.selected",
|
|
"mail.listing.plain.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.body",
|
|
"mail.listing.attachment_flag",
|
|
"mail.listing.thread_snooze_flag",
|
|
];
|
|
|
|
/// `ThemeAttributeInner` but with the links resolved.
|
|
#[derive(Debug, PartialEq, Eq, Clone, Default, Copy, Serialize, Deserialize)]
|
|
pub struct ThemeAttribute {
|
|
pub fg: Color,
|
|
pub bg: Color,
|
|
pub attrs: Attr,
|
|
}
|
|
|
|
/// Holds {fore,back}ground color and terminal attribute values.
|
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
pub struct ThemeAttributeInner {
|
|
#[serde(default)]
|
|
fg: ThemeValue<Color>,
|
|
#[serde(default)]
|
|
bg: ThemeValue<Color>,
|
|
#[serde(default)]
|
|
attrs: ThemeValue<Attr>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
/// Holds either an actual value or refers to the key name of the attribute that holds the value.
|
|
pub enum ThemeValue<T> {
|
|
Value(T),
|
|
Link(Cow<'static, str>),
|
|
}
|
|
|
|
impl<T> From<&'static str> for ThemeValue<T> {
|
|
fn from(from: &'static str) -> Self {
|
|
ThemeValue::Link(from.into())
|
|
}
|
|
}
|
|
|
|
impl From<Color> for ThemeValue<Color> {
|
|
fn from(from: Color) -> Self {
|
|
ThemeValue::Value(from)
|
|
}
|
|
}
|
|
|
|
impl From<Attr> for ThemeValue<Attr> {
|
|
fn from(from: Attr) -> Self {
|
|
ThemeValue::Value(from)
|
|
}
|
|
}
|
|
|
|
impl Default for ThemeValue<Color> {
|
|
fn default() -> Self {
|
|
ThemeValue::Value(Color::Default)
|
|
}
|
|
}
|
|
|
|
impl Default for ThemeValue<Attr> {
|
|
fn default() -> Self {
|
|
ThemeValue::Value(Attr::Default)
|
|
}
|
|
}
|
|
|
|
impl<'de> Deserialize<'de> for ThemeValue<Attr> {
|
|
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
{
|
|
if let Ok(s) = <String>::deserialize(deserializer) {
|
|
if let Ok(c) = Attr::from_string_de::<'de, D>(s.clone()) {
|
|
Ok(ThemeValue::Value(c))
|
|
} else {
|
|
Ok(ThemeValue::Link(s.into()))
|
|
}
|
|
} else {
|
|
Err(de::Error::custom("invalid theme attribute value"))
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<T: Serialize> Serialize for ThemeValue<T> {
|
|
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
|
where
|
|
S: Serializer,
|
|
{
|
|
match self {
|
|
ThemeValue::Value(s) => s.serialize(serializer),
|
|
ThemeValue::Link(s) => serializer.serialize_str(s.as_ref()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'de> Deserialize<'de> for ThemeValue<Color> {
|
|
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
{
|
|
if let Ok(s) = <String>::deserialize(deserializer) {
|
|
if let Ok(c) = Color::from_string_de::<'de, D>(s.clone()) {
|
|
Ok(ThemeValue::Value(c))
|
|
} else {
|
|
Ok(ThemeValue::Link(s.into()))
|
|
}
|
|
} else {
|
|
Err(de::Error::custom("invalid theme color value"))
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct Theme {
|
|
#[serde(default)]
|
|
pub light: HashMap<Cow<'static, str>, ThemeAttributeInner>,
|
|
#[serde(default)]
|
|
pub dark: HashMap<Cow<'static, str>, ThemeAttributeInner>,
|
|
#[serde(flatten, default)]
|
|
pub other_themes: HashMap<String, HashMap<Cow<'static, str>, ThemeAttributeInner>>,
|
|
}
|
|
|
|
impl Theme {
|
|
pub fn validate(&self) -> Result<()> {
|
|
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
|
|
)));
|
|
}
|
|
}
|
|
let hash_set: HashSet<&'static str> = DEFAULT_KEYS.into_iter().map(|k| *k).collect();
|
|
let keys = self
|
|
.light
|
|
.keys()
|
|
.filter_map(|k| {
|
|
if !hash_set.contains(&k.as_ref()) {
|
|
Some(k.as_ref())
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect::<SmallVec<[&'_ str; 128]>>();
|
|
if !keys.is_empty() {
|
|
return Err(format!(
|
|
"light theme contains unrecognized theme keywords: {}",
|
|
keys.join(", ")
|
|
)
|
|
.into());
|
|
}
|
|
let keys = self
|
|
.dark
|
|
.keys()
|
|
.filter_map(|k| {
|
|
if !hash_set.contains(&k.as_ref()) {
|
|
Some(k.as_ref())
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect::<SmallVec<[&'_ str; 128]>>();
|
|
if !keys.is_empty() {
|
|
return Err(format!(
|
|
"light theme contains unrecognized theme keywords: {}",
|
|
keys.join(", ")
|
|
)
|
|
.into());
|
|
}
|
|
for (name, t) in self.other_themes.iter() {
|
|
let keys = t
|
|
.keys()
|
|
.filter_map(|k| {
|
|
if !hash_set.contains(&k.as_ref()) {
|
|
Some(k.as_ref())
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect::<SmallVec<[&'_ str; 128]>>();
|
|
if !keys.is_empty() {
|
|
return Err(format!(
|
|
"`{}` theme contains unrecognized theme keywords: {}",
|
|
name,
|
|
keys.join(", ")
|
|
)
|
|
.into());
|
|
}
|
|
}
|
|
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, k)).unwrap(),
|
|
toml::to_string(&unlink_bg(&theme, 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 Theme {
|
|
fn default() -> Theme {
|
|
let mut light = HashMap::default();
|
|
let mut dark = HashMap::default();
|
|
let other_themes = HashMap::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");
|
|
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::Byte(88) }, light = { fg: Color::Byte(219), bg: Color::Byte(88) });
|
|
|
|
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.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_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_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.unseen",
|
|
dark = {
|
|
fg: Color::Byte(0),
|
|
bg: Color::Byte(251)
|
|
|
|
},
|
|
light = {
|
|
fg: Color::Byte(0),
|
|
bg: Color::Byte(251)
|
|
}
|
|
);
|
|
add!("mail.listing.compact.selected",
|
|
dark = {
|
|
bg: Color::Byte(210)
|
|
},
|
|
light = {
|
|
bg: Color::Byte(210)
|
|
}
|
|
);
|
|
add!(
|
|
"mail.listing.compact.highlighted",
|
|
dark = {
|
|
bg: Color::Byte(246)
|
|
},
|
|
light = {
|
|
bg: Color::Byte(244)
|
|
}
|
|
);
|
|
|
|
/* ConversationsListing */
|
|
|
|
add!("mail.listing.conversations");
|
|
add!("mail.listing.conversations.subject");
|
|
add!("mail.listing.conversations.from");
|
|
add!("mail.listing.conversations.date");
|
|
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),
|
|
},
|
|
light = {
|
|
bg: Color::Byte(246)
|
|
}
|
|
);
|
|
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.unseen",
|
|
dark = {
|
|
fg: Color::Byte(0),
|
|
bg: Color::Byte(251)
|
|
|
|
},
|
|
light = {
|
|
fg: Color::Byte(0),
|
|
bg: Color::Byte(251)
|
|
}
|
|
);
|
|
add!("mail.listing.plain.selected",
|
|
dark = {
|
|
bg: Color::Byte(210)
|
|
},
|
|
light = {
|
|
bg: Color::Byte(210)
|
|
}
|
|
);
|
|
add!(
|
|
"mail.listing.plain.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.body");
|
|
|
|
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,
|
|
}
|
|
);
|
|
|
|
Theme {
|
|
light,
|
|
dark,
|
|
other_themes,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Serialize for Theme {
|
|
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
|
where
|
|
S: Serializer,
|
|
{
|
|
let mut dark: HashMap<Cow<'static, str>, ThemeAttribute> = Default::default();
|
|
let mut light: HashMap<Cow<'static, str>, ThemeAttribute> = Default::default();
|
|
let mut other_themes: HashMap<String, _> = Default::default();
|
|
|
|
for k in self.dark.keys() {
|
|
dark.insert(
|
|
k.clone(),
|
|
ThemeAttribute {
|
|
fg: unlink_fg(&self.dark, k),
|
|
bg: unlink_bg(&self.dark, k),
|
|
attrs: unlink_attrs(&self.dark, k),
|
|
},
|
|
);
|
|
}
|
|
|
|
for k in self.light.keys() {
|
|
light.insert(
|
|
k.clone(),
|
|
ThemeAttribute {
|
|
fg: unlink_fg(&self.light, k),
|
|
bg: unlink_bg(&self.light, k),
|
|
attrs: unlink_attrs(&self.light, k),
|
|
},
|
|
);
|
|
}
|
|
|
|
for (name, t) in self.other_themes.iter() {
|
|
let mut new_map: HashMap<Cow<'static, str>, ThemeAttribute> = Default::default();
|
|
|
|
for k in t.keys() {
|
|
new_map.insert(
|
|
k.clone(),
|
|
ThemeAttribute {
|
|
fg: unlink_fg(&t, k),
|
|
bg: unlink_bg(&t, 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: &HashMap<Cow<'static, str>, ThemeAttributeInner>,
|
|
) -> std::result::Result<(), String> {
|
|
enum Course {
|
|
Fg,
|
|
Bg,
|
|
Attrs,
|
|
}
|
|
fn is_cyclic_util<'a>(
|
|
course: &Course,
|
|
k: &'a Cow<'static, str>,
|
|
visited: &mut HashMap<&'a Cow<'static, str>, bool>,
|
|
stack: &mut HashMap<&'a Cow<'static, str>, bool>,
|
|
path: &mut SmallVec<[&'a Cow<'static, str>; 16]>,
|
|
theme: &'a HashMap<Cow<'static, str>, ThemeAttributeInner>,
|
|
) -> bool {
|
|
if !visited[k] {
|
|
visited.entry(k).and_modify(|e| *e = true);
|
|
stack.entry(k).and_modify(|e| *e = true);
|
|
|
|
match course {
|
|
Course::Fg => match theme[k].fg {
|
|
ThemeValue::Link(ref l) => {
|
|
path.push(l);
|
|
if !visited[l] && is_cyclic_util(course, l, visited, stack, path, theme) {
|
|
return true;
|
|
} else if stack[l] {
|
|
return true;
|
|
}
|
|
path.pop();
|
|
}
|
|
_ => {}
|
|
},
|
|
Course::Bg => match theme[k].bg {
|
|
ThemeValue::Link(ref l) => {
|
|
path.push(l);
|
|
if !visited[l] && is_cyclic_util(course, l, visited, stack, path, theme) {
|
|
return true;
|
|
} else if stack[l] {
|
|
return true;
|
|
}
|
|
path.pop();
|
|
}
|
|
_ => {}
|
|
},
|
|
Course::Attrs => match theme[k].attrs {
|
|
ThemeValue::Link(ref l) => {
|
|
path.push(l);
|
|
if !visited[l] && is_cyclic_util(course, l, visited, stack, path, theme) {
|
|
return true;
|
|
} else if stack[l] {
|
|
return true;
|
|
}
|
|
path.pop();
|
|
}
|
|
_ => {}
|
|
},
|
|
}
|
|
}
|
|
stack.entry(k).and_modify(|e| *e = false);
|
|
return false;
|
|
}
|
|
|
|
let mut path = SmallVec::new();
|
|
let mut visited = theme
|
|
.keys()
|
|
.map(|k| (k, false))
|
|
.collect::<HashMap<&Cow<'static, str>, bool>>();
|
|
|
|
let mut stack = theme
|
|
.keys()
|
|
.map(|k| (k, false))
|
|
.collect::<HashMap<&Cow<'static, str>, bool>>();
|
|
for k in theme.keys() {
|
|
for course in [Course::Fg, Course::Bg, Course::Attrs].iter() {
|
|
path.push(k);
|
|
if is_cyclic_util(course, k, &mut visited, &mut stack, &mut path, &theme) {
|
|
let path = path
|
|
.into_iter()
|
|
.map(|k| k.to_string())
|
|
.collect::<Vec<String>>();
|
|
return Err(format!(
|
|
"{} {}",
|
|
match course {
|
|
Course::Fg => "fg: ",
|
|
Course::Bg => "bg: ",
|
|
Course::Attrs => "attrs: ",
|
|
},
|
|
path.join(" -> ")
|
|
));
|
|
}
|
|
for v in visited.values_mut() {
|
|
*v = false;
|
|
}
|
|
path.pop();
|
|
}
|
|
}
|
|
|
|
return Ok(());
|
|
}
|