use super::{colors::Color, TextModifier};
use anyhow::{anyhow, Error};
use serde::{Deserialize, Serialize};
use std::{
collections::{BTreeMap, HashMap},
fmt::{Debug, Display, Formatter},
str::FromStr,
};
use tui::style::Style;
#[derive(Serialize, Deserialize, Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct TextStyle {
bg: Option<Color>,
fg: Option<Color>,
modifier: Option<TextModifier>,
}
const PRIORITIES: [&str; 27] = [
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S",
"T", "U", "V", "W", "X", "Y", "Z", "empty",
];
impl TextStyle {
pub fn bg(mut self, bg: Color) -> Self {
self.bg = Some(bg);
self
}
pub fn fg(mut self, fg: Color) -> Self {
self.fg = Some(fg);
self
}
pub fn modifier(mut self, modifier: TextModifier) -> Self {
self.modifier = Some(modifier);
self
}
pub fn is_some(&self) -> bool {
self.bg.is_some() || self.fg.is_some() || self.modifier.is_some()
}
pub fn combine(&self, additional: &Self) -> TextStyle {
let mut new = *self;
new.add_style(additional);
new
}
pub fn add_style(&mut self, additional: &Self) {
if let Some(bg) = additional.bg {
self.bg = Some(bg);
}
if let Some(fg) = additional.fg {
self.fg = Some(fg);
}
if let Some(modifier) = additional.modifier {
self.modifier = Some(modifier);
}
}
pub fn get_style(&self) -> Style {
let mut style = Style::default();
if let Some(c) = self.bg {
style = style.bg(*c);
}
if let Some(c) = self.fg {
style = style.fg(*c);
}
if let Some(s) = self.modifier {
style = style.add_modifier(s.into());
}
style
}
}
impl Display for TextStyle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(
&[
self.fg.map(|f| f.to_string()),
self.bg.map(|b| format!("^{b}")),
self.modifier.map(|m| format!("{m:?}")),
]
.into_iter()
.flatten()
.collect::<Vec<_>>()
.join(" "),
)
}
}
impl FromStr for TextStyle {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut ret = TextStyle::default();
for word in s.split_whitespace() {
if let Some(stripped) = word.strip_prefix('^') {
match Color::from_str(stripped) {
Ok(c) => ret = ret.bg(c),
Err(_) => return Err(anyhow!("Style '{word}' is invalid")),
}
} else if let Ok(color) = Color::from_str(word) {
ret = ret.fg(color);
} else if let Ok(modifier) = TextModifier::from_str(word) {
ret = ret.modifier(modifier);
} else {
match TextStyleList::default().0.get(word) {
Some(style) => ret = ret.combine(style),
None => return Err(anyhow!("Style '{word}' is invalid")),
}
}
}
Ok(ret)
}
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct TextStyleList(HashMap<String, TextStyle>);
impl TextStyleList {
pub fn get_style(&self, index: u8) -> Style {
self.get_text_style(index).get_style()
}
pub fn get_text_style(&self, index: u8) -> TextStyle {
match self.0.get(&todo_txt::Priority::from(index).to_string()) {
Some(item) => *item,
None => TextStyle::default(),
}
}
pub fn get_style_from_str(&self, s: &str) -> Option<TextStyle> {
self.0.get(s).copied()
}
}
impl Debug for TextStyleList {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self.0.iter().collect::<BTreeMap<_, _>>())
}
}
impl Default for TextStyleList {
fn default() -> Self {
let mut items = HashMap::new();
items.insert(String::from("A"), TextStyle::default().fg(Color::red()));
items.insert(String::from("B"), TextStyle::default().fg(Color::yellow()));
items.insert(String::from("C"), TextStyle::default().fg(Color::blue()));
Self(items)
}
}
impl Display for TextStyleList {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(
&self
.0
.iter()
.map(|(key, style)| format!("{}: {}", key, style))
.collect::<Vec<_>>()
.join(", "),
)?;
Ok(())
}
}
impl FromStr for TextStyleList {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut ret = HashMap::new();
for s in s.split(',') {
match s.find(':') {
Some(index) => {
for priority in PRIORITIES {
let key = s[..index].trim();
if priority == key {
ret.insert(key.to_string(), TextStyle::from_str(s[..index].trim())?);
break;
}
}
}
None => return Err(anyhow!("Style '{s}' is invalid")),
}
}
Ok(TextStyleList(ret))
}
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use tui::style::Color as tuiColor;
use super::*;
#[test]
fn text_style() {
let style = TextStyle::default();
assert_eq!(style.bg, None);
assert_eq!(style.fg, None);
assert_eq!(style.modifier, None);
assert!(!style.is_some());
let style = style
.bg(Color::red())
.fg(Color::green())
.modifier(TextModifier::Bold);
assert_eq!(style.bg, Some(Color::red()));
assert_eq!(style.fg, Some(Color::green()));
assert_eq!(style.modifier, Some(TextModifier::Bold));
assert!(style.is_some());
let _ = style.get_style();
}
#[test]
fn combine_styles() {
let style = TextStyle::default()
.bg(Color::red())
.modifier(TextModifier::Bold)
.combine(
&TextStyle::default()
.fg(Color::green())
.modifier(TextModifier::Italic),
);
assert_eq!(style.bg, Some(Color::red()));
assert_eq!(style.fg, Some(Color::green()));
assert_eq!(style.modifier, Some(TextModifier::Italic));
let style = TextStyle::default()
.bg(Color::red())
.combine(&TextStyle::default().bg(Color::yellow()));
assert_eq!(style.bg, Some(Color::yellow()));
assert_eq!(style.fg, None);
assert_eq!(style.modifier, None);
}
#[test]
fn text_style_list() {
let style = TextStyleList::default().get_style(0);
assert_eq!(style.fg, Some(tuiColor::Red));
assert_eq!(style.bg, None);
assert!(style.add_modifier.is_empty());
}
#[test]
fn from_str() -> Result<()> {
assert_eq!(
TextStyle::from_str("red")?,
TextStyle::default().fg(Color::red())
);
assert_eq!(
TextStyle::from_str("^red").unwrap(),
TextStyle::default().bg(Color::red())
);
assert_eq!(
TextStyle::from_str("green ^blue").unwrap(),
TextStyle::default().fg(Color::green()).bg(Color::blue())
);
assert_eq!(
TextStyle::from_str("bold").unwrap(),
TextStyle::default().modifier(TextModifier::Bold)
);
assert_eq!(
TextStyle::from_str("italic").unwrap(),
TextStyle::default().modifier(TextModifier::Italic)
);
assert_eq!(
TextStyle::from_str("underline").unwrap(),
TextStyle::default().modifier(TextModifier::Underlined)
);
assert_eq!(
TextStyle::from_str("red bold ^blue italic").unwrap(),
TextStyle::default()
.fg(Color::red())
.modifier(TextModifier::Bold)
.bg(Color::blue())
.modifier(TextModifier::Italic)
);
Ok(())
}
#[test]
fn from_str_err() {
assert_eq!(
TextStyle::from_str("invalid_color")
.unwrap_err()
.to_string(),
"Style 'invalid_color' is invalid"
);
assert_eq!(
TextStyle::from_str("^bg_invalid_color")
.unwrap_err()
.to_string(),
"Style '^bg_invalid_color' is invalid"
);
assert_eq!(
TextStyle::from_str("invalid_modifier")
.unwrap_err()
.to_string(),
"Style 'invalid_modifier' is invalid"
);
assert_eq!(
TextStyle::from_str("unknown_style")
.unwrap_err()
.to_string(),
"Style 'unknown_style' is invalid"
);
}
#[test]
fn display() {
assert_eq!(
&format!(
"{}",
TextStyle {
bg: None,
fg: Some(Color::green()),
modifier: None
}
),
"Green"
);
assert_eq!(
&format!(
"{}",
TextStyle {
bg: Some(Color::red()),
fg: None,
modifier: None
}
),
"^Red"
);
assert_eq!(
&format!(
"{}",
TextStyle {
bg: None,
fg: None,
modifier: Some(TextModifier::Bold)
}
),
"Bold"
);
assert_eq!(
&format!(
"{}",
TextStyle {
bg: Some(Color::yellow()),
fg: Some(Color::black()),
modifier: Some(TextModifier::Italic)
}
),
"Black ^Yellow Italic"
);
}
#[test]
fn text_style_list_from_str() -> Result<()> {
let mut expected = HashMap::<String, TextStyle>::new();
expected.insert("A".to_string(), TextStyle::default().fg(Color::red()));
assert_eq!(TextStyleList::from_str("A:green")?, TextStyleList(expected));
Ok(())
}
}