todotxt-tui 0.3.0

Todo.txt TUI is a highly customizable terminal-based application for managing your todo tasks. It follows the todo.txt format and offers a wide range of configuration options to suit your needs.
Documentation
use anyhow::anyhow;
use serde::{de, Deserialize, Serialize};
use std::{
    fmt::Display,
    ops::{Deref, DerefMut},
    str::FromStr,
};
use tui::style::Color as tuiColor;

/// Wrapper around [`tui::style::Color`] that adds parsing, serialization,
/// and convenient constructor methods for terminal colors.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Color(pub tuiColor);

impl Color {
    /// Returns the black color.
    pub fn black() -> Self {
        Self(tuiColor::Black)
    }
    /// Returns the red color.
    pub fn red() -> Self {
        Self(tuiColor::Red)
    }
    /// Returns the green color.
    pub fn green() -> Self {
        Self(tuiColor::Green)
    }
    /// Returns the yellow color.
    pub fn yellow() -> Self {
        Self(tuiColor::Yellow)
    }
    /// Returns the blue color.
    pub fn blue() -> Self {
        Self(tuiColor::Blue)
    }
    /// Returns the magenta color.
    pub fn magenta() -> Self {
        Self(tuiColor::Magenta)
    }
    /// Returns the cyan color.
    pub fn cyan() -> Self {
        Self(tuiColor::Cyan)
    }
    /// Returns the gray color.
    pub fn gray() -> Self {
        Self(tuiColor::Gray)
    }
    /// Returns the dark gray color.
    pub fn darkgray() -> Self {
        Self(tuiColor::DarkGray)
    }
    /// Returns the light red color.
    pub fn lightred() -> Self {
        Self(tuiColor::LightRed)
    }
    /// Returns the light green color.
    pub fn lightgreen() -> Self {
        Self(tuiColor::LightGreen)
    }
    /// Returns the light yellow color.
    pub fn lightyellow() -> Self {
        Self(tuiColor::LightYellow)
    }
    /// Returns the light blue color.
    pub fn lightblue() -> Self {
        Self(tuiColor::LightBlue)
    }
    /// Returns the light magenta color.
    pub fn lightmagenta() -> Self {
        Self(tuiColor::LightMagenta)
    }
    /// Returns the light cyan color.
    pub fn lightcyan() -> Self {
        Self(tuiColor::LightCyan)
    }
    /// Returns the white color.
    pub fn white() -> Self {
        Self(tuiColor::White)
    }
}

impl Deref for Color {
    type Target = tui::style::Color;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl DerefMut for Color {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

impl Display for Color {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self.0 {
            tuiColor::Rgb(r, g, b) => write!(f, "#{r:02x}{g:02x}{b:02x}"),
            tuiColor::Indexed(i) => write!(f, "{i}"),
            _ => write!(f, "{:?}", self.0),
        }
    }
}

impl From<tuiColor> for Color {
    fn from(value: tuiColor) -> Self {
        Self(value)
    }
}

impl FromStr for Color {
    type Err = anyhow::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let parse_hex = |hex: &str| -> Result<u8, Self::Err> {
            u8::from_str_radix(hex, 16)
                .map_err(|_| anyhow!("String '{}' is not valid hex color.", s))
        };
        if let Ok(index) = s.parse::<u8>() {
            return Ok(Self(tuiColor::Indexed(index)));
        }
        let lower = s.to_lowercase();
        use tuiColor::*;
        Ok(Self(match lower.as_str() {
            "black" => Black,
            "red" => Red,
            "green" => Green,
            "yellow" => Yellow,
            "blue" => Blue,
            "magenta" => Magenta,
            "cyan" => Cyan,
            "gray" => Gray,
            "darkgray" => DarkGray,
            "lightred" => LightRed,
            "lightgreen" => LightGreen,
            "lightyellow" => LightYellow,
            "lightblue" => LightBlue,
            "lightmagenta" => LightMagenta,
            "lightcyan" => LightCyan,
            "white" => White,
            _ if lower.starts_with('#') && lower.len() == 7 => Rgb(
                parse_hex(&lower[1..3])?,
                parse_hex(&lower[3..5])?,
                parse_hex(&lower[5..7])?,
            ),
            _ => return Err(anyhow!("String '{}' is not valid color.", s)),
        }))
    }
}

impl Serialize for Color {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_str(&self.to_string())
    }
}

impl<'de> Deserialize<'de> for Color {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let s: String = Deserialize::deserialize(deserializer)?;
        Self::from_str(&s).map_err(de::Error::custom)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use anyhow::Result;

    #[test]
    fn from_str() -> Result<()> {
        assert_eq!(Color(tuiColor::Red), Color::from_str("red")?);
        assert_eq!(Color(tuiColor::Yellow), Color::from_str("YeLlOW")?);
        assert_eq!(Color(tuiColor::Rgb(3, 4, 5)), Color::from_str("#030405")?);
        Ok(())
    }

    #[test]
    fn to_string() {
        assert_eq!(Color(tuiColor::Green).to_string(), "Green");
        assert_eq!(Color(tuiColor::Black).to_string(), "Black");
        assert_eq!(Color(tuiColor::Rgb(6, 7, 8)).to_string(), "#060708");
    }
}