eilmeldung 1.4.3

a feature-rich TUI RSS Reader based on the news-flash library
use std::str::FromStr;

use getset::Getters;
use ratatui::style::{Color, Modifier, Style};

#[derive(Debug, Clone, serde::Deserialize, Getters)]
#[serde(default)]
#[getset(get = "pub")]
pub struct ColorPalette {
    background: Color,
    foreground: Color,
    muted: Color,
    highlight: Color,
    flagged: Color,
    accent_primary: Color,
    accent_secondary: Color,
    accent_tertiary: Color,
    accent_quaternary: Color,

    info: Color,
    warning: Color,
    error: Color,
}

impl Default for ColorPalette {
    fn default() -> Self {
        use Color as C;
        Self {
            background: C::Black,
            foreground: C::White,
            muted: C::DarkGray,
            highlight: C::Yellow,
            flagged: C::Red,
            accent_primary: C::Magenta,
            accent_secondary: C::Blue,
            accent_tertiary: C::Cyan,
            accent_quaternary: C::Yellow,

            info: C::Magenta,
            warning: C::Yellow,
            error: C::Red,
        }
    }
}

#[derive(Debug, Copy, Default, Clone)]
pub enum StyleColor {
    #[default]
    None,

    Background,
    Foreground,
    Muted,
    Highlight,
    Flagged,
    AccentPrimary,
    AccentSecondary,
    AccentTertiary,
    AccentQuaternary,
    Info,
    Warning,
    Error,

    Custom(Color),
}

impl<'de> serde::de::Deserialize<'de> for StyleColor {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;

        use StyleColor as C;

        Ok(match s.trim() {
            "none" => C::None,
            "background" => C::Background,
            "foreground" => C::Foreground,
            "muted" => C::Muted,
            "highlight" => C::Highlight,
            "flagged" => C::Flagged,
            "accent_primary" => C::AccentPrimary,
            "accent_secondary" => C::AccentSecondary,
            "accent_tertiary" => C::AccentTertiary,
            "accent_quaternary" => C::AccentQuaternary,
            "info" => C::Info,
            "warning" => C::Warning,
            "error" => C::Error,
            s => C::Custom(
                Color::from_str(s)
                    .map_err(|_| serde::de::Error::custom(format!("unable to parse color: {s}")))?,
            ),
        })
    }
}

#[derive(Debug, Default, Clone, serde::Deserialize)]
pub struct ComponentStyle {
    #[serde(default)]
    fg: StyleColor,

    #[serde(default)]
    bg: StyleColor,

    #[serde(default)]
    mods: Vec<StyleModifier>,
}

#[derive(Debug, Clone, Copy, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StyleModifier {
    Bold,
    Dim,
    Italic,
    Underlined,
    SlowBlink,
    RapidBlink,
    Reversed,
    Hidden,
    CrossedOut,
}

impl StyleModifier {
    fn to_modifier(self) -> Modifier {
        use StyleModifier as S;
        match self {
            S::Bold => Modifier::BOLD,
            S::Dim => Modifier::DIM,
            S::Italic => Modifier::ITALIC,
            S::Underlined => Modifier::UNDERLINED,
            S::SlowBlink => Modifier::SLOW_BLINK,
            S::RapidBlink => Modifier::RAPID_BLINK,
            S::Reversed => Modifier::REVERSED,
            S::Hidden => Modifier::HIDDEN,
            S::CrossedOut => Modifier::CROSSED_OUT,
        }
    }
}

impl ComponentStyle {
    fn fg(self, fg: StyleColor) -> Self {
        Self { fg, ..self }
    }

    fn bg(self, bg: StyleColor) -> Self {
        Self { bg, ..self }
    }

    fn mods(self, mods: &[StyleModifier]) -> Self {
        Self {
            mods: mods.to_owned(),
            ..self
        }
    }

    fn modifiers(&self) -> Modifier {
        self.mods
            .iter()
            .fold(Modifier::default(), |mut modifiers, modifier| {
                modifiers |= modifier.to_modifier();
                modifiers
            })
    }
}

#[derive(Debug, Clone, serde::Deserialize)]
#[serde(default)]
pub struct StyleSet {
    header: ComponentStyle,
    paragraph: ComponentStyle,
    article: ComponentStyle,
    feed: ComponentStyle,
    category: ComponentStyle,
    tag: ComponentStyle,
    query: ComponentStyle,
    yanked: ComponentStyle,

    border: ComponentStyle,
    border_focused: ComponentStyle,
    statusbar: ComponentStyle,
    command_input: ComponentStyle,
    inactive: ComponentStyle,

    tooltip_info: ComponentStyle,
    tooltip_warning: ComponentStyle,
    tooltip_error: ComponentStyle,

    unread: ComponentStyle,
    unread_count: ComponentStyle,
    marked_count: ComponentStyle,
    read: ComponentStyle,
    selected: ComponentStyle,
    highlighted: ComponentStyle,
    flagged: ComponentStyle,
}

impl Default for StyleSet {
    fn default() -> Self {
        use StyleColor as C;
        use StyleModifier as M;
        Self {
            header: ComponentStyle::default().fg(C::AccentPrimary),
            paragraph: ComponentStyle::default().fg(C::Foreground),
            article: ComponentStyle::default().fg(C::Foreground),
            feed: ComponentStyle::default().fg(C::AccentPrimary),
            category: ComponentStyle::default().fg(C::AccentSecondary),
            tag: ComponentStyle::default().fg(C::AccentTertiary),
            query: ComponentStyle::default().fg(C::AccentQuaternary),
            yanked: ComponentStyle::default()
                .fg(C::Highlight)
                .mods(&[M::Reversed]),

            border: ComponentStyle::default().fg(C::Muted),
            border_focused: ComponentStyle::default().fg(C::AccentPrimary),
            statusbar: ComponentStyle::default()
                .fg(C::AccentPrimary)
                .mods(&[M::Reversed]),
            command_input: ComponentStyle::default().fg(C::Foreground).bg(C::Muted),
            inactive: ComponentStyle::default().fg(C::Muted),

            tooltip_info: ComponentStyle::default().fg(C::Info).mods(&[M::Reversed]),
            tooltip_warning: ComponentStyle::default()
                .fg(C::Warning)
                .mods(&[M::Reversed]),
            tooltip_error: ComponentStyle::default().fg(C::Error).mods(&[M::Reversed]),

            unread: ComponentStyle::default().mods(&[M::Bold]),
            read: ComponentStyle::default().mods(&[M::Dim]),
            selected: ComponentStyle::default().mods(&[M::Reversed]),
            highlighted: ComponentStyle::default()
                .fg(C::Highlight)
                .mods(&[M::Italic]),

            flagged: ComponentStyle::default().fg(C::Flagged),

            unread_count: ComponentStyle::default().mods(&[M::Italic]),
            marked_count: ComponentStyle::default().mods(&[M::Italic]),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Deserialize, Getters)]
#[serde(default)]
#[getset(get = "pub")]
pub struct Theme {
    color_palette: ColorPalette,
    style_set: StyleSet,
}

macro_rules! component_funs {
    {$($prop:ident),*} => {
        $(pub fn $prop(&self) -> Style {
            self.to_style(&self.style_set.$prop)
        })*
    };
}

macro_rules! patch_funs {
    {$($prop:ident),*} => {
        $(pub fn $prop(&self, style: &Style) -> Style {
            style.patch(self.to_style(&self.style_set.$prop))
        })*
    };
}

impl Theme {
    pub fn color(&self, style_color: StyleColor) -> Option<Color> {
        use StyleColor as SC;
        Some(match style_color {
            SC::None => return None,
            SC::Background => self.color_palette.background,
            SC::Foreground => self.color_palette.foreground,
            SC::Muted => self.color_palette.muted,
            SC::Highlight => self.color_palette.highlight,
            SC::Flagged => self.color_palette.flagged,
            SC::AccentPrimary => self.color_palette.accent_primary,
            SC::AccentSecondary => self.color_palette.accent_secondary,
            SC::AccentTertiary => self.color_palette.accent_tertiary,
            SC::AccentQuaternary => self.color_palette.accent_quaternary,
            SC::Info => self.color_palette.info,
            SC::Warning => self.color_palette.warning,
            SC::Error => self.color_palette.error,
            SC::Custom(color) => color,
        })
    }

    pub fn eff_border(&self, is_focused: bool) -> Style {
        if is_focused {
            self.border_focused()
        } else {
            self.border()
        }
    }

    pub fn to_style(&self, component_style: &ComponentStyle) -> Style {
        Style {
            fg: self.color(component_style.fg),
            bg: self.color(component_style.bg),
            add_modifier: component_style.modifiers(),
            ..Default::default()
        }
    }

    patch_funs! {
        unread,
        read,
        selected,
        highlighted,
        flagged
    }

    component_funs! {
      header,
      paragraph,
      article,
      feed,
      category,
      tag,
      query,
      yanked,
      border,
      border_focused,
      statusbar,
      command_input,
      inactive,
      tooltip_info,
      tooltip_warning,
      tooltip_error,
      unread_count,
      marked_count
    }
}