synd-term 0.3.2

terminal feed viewer
Documentation
use ratatui::style::{Color, Modifier, Style, Stylize};

#[derive(Clone)]
pub struct Theme {
    pub name: &'static str,
    pub base: Style,
    pub application_title: Style,
    pub login: LoginTheme,
    pub tabs: Style,
    pub tabs_selected: Style,
    pub prompt: PromptTheme,
    pub subscription: SubscriptionTheme,
    pub entries: EntriesTheme,
    pub error: ErrorTheme,
    pub default_icon_fg: Color,
    pub requirement: RequirementLabelTheme,
    pub selection_popup: SelectionPopup,
}

#[derive(Clone)]
pub struct LoginTheme {
    pub title: Style,
    pub selected_auth_provider_item: Style,
}

#[derive(Clone)]
pub struct ErrorTheme {
    pub message: Style,
}

#[derive(Clone)]
pub struct PromptTheme {
    pub key: Style,
    pub key_desc: Style,
    pub background: Style,
}

#[derive(Clone)]
pub struct SubscriptionTheme {
    pub background: Style,
    pub header: Style,
    pub selected_feed: Style,
}

#[derive(Clone)]
pub struct EntriesTheme {
    pub header: Style,
    pub selected_entry: Style,
    pub summary: Style,
}

#[derive(Clone)]
pub struct RequirementLabelTheme {
    pub must: Color,
    pub should: Color,
    pub may: Color,
    pub fg: Color,
}

#[derive(Clone)]
pub struct SelectionPopup {
    pub highlight: Style,
}

#[derive(Clone, Debug)]
pub struct Palette {
    name: &'static str,
    bg: Color,
    fg: Color,
    fg_inactive: Color,
    fg_focus: Color,
    error: Color,
}

impl Palette {
    pub fn dracula() -> Self {
        Self {
            name: "dracula",
            bg: Color::Rgb(0x28, 0x2a, 0x36),
            fg: Color::Rgb(0xf8, 0xf8, 0xf2),
            fg_inactive: Color::Rgb(0x62, 0x72, 0xa4),
            fg_focus: Color::Rgb(0xff, 0x79, 0xc6),
            error: Color::Rgb(0xff, 0x55, 0x55),
        }
    }

    pub fn eldritch() -> Self {
        Self {
            name: "eldritch",
            bg: Color::Rgb(0x21, 0x23, 0x37),
            fg: Color::Rgb(0xeb, 0xfa, 0xfa),
            fg_inactive: Color::Rgb(0x70, 0x81, 0xd0),
            fg_focus: Color::Rgb(0x37, 0xf4, 0x99),
            error: Color::Rgb(0xf1, 0x6c, 0x75),
        }
    }

    pub fn helix() -> Self {
        Self {
            name: "helix",
            bg: Color::Rgb(0x3b, 0x22, 0x4c),
            fg: Color::Rgb(0xa4, 0xa0, 0xe8),
            fg_inactive: Color::Rgb(0x69, 0x7c, 0x81),
            fg_focus: Color::Rgb(0xff, 0xff, 0xff),
            error: Color::Rgb(0xf4, 0x78, 0x68),
        }
    }

    pub fn ferra() -> Self {
        Self {
            name: "ferra",
            bg: Color::Rgb(0x2b, 0x29, 0x2d),
            fg: Color::Rgb(0xfe, 0xcd, 0xb2),
            fg_inactive: Color::Rgb(0x6F, 0x5D, 0x63),
            fg_focus: Color::Rgb(0xff, 0xa0, 0x7a),
            error: Color::Rgb(0xe0, 0x6b, 0x75),
        }
    }

    pub fn solarized_dark() -> Self {
        Self {
            name: "solarized_dark",
            bg: Color::Rgb(0x00, 0x2b, 0x36),
            fg: Color::Rgb(0x93, 0xa1, 0xa1),
            fg_inactive: Color::Rgb(0x58, 0x6e, 0x75),
            fg_focus: Color::Rgb(0x26, 0x8b, 0xd2),
            error: Color::Rgb(0xdc, 0x32, 0x2f),
        }
    }
}

impl Theme {
    #[allow(clippy::needless_pass_by_value)]
    pub fn with_palette(p: Palette) -> Self {
        let Palette {
            name,
            bg,
            fg,
            fg_inactive,
            fg_focus,
            error,
        } = p;

        Self {
            name,
            base: Style::new().bg(bg).fg(fg),
            application_title: Style::new().fg(fg).bg(bg).add_modifier(Modifier::BOLD),
            login: LoginTheme {
                title: Style::new().add_modifier(Modifier::BOLD),
                selected_auth_provider_item: Style::new().add_modifier(Modifier::BOLD),
            },
            tabs: Style::new().fg(fg),
            tabs_selected: Style::new().fg(fg_focus).bold(),
            prompt: PromptTheme {
                key: Style::new().fg(fg_inactive).bg(bg),
                key_desc: Style::new().fg(fg_inactive).bg(bg),
                background: Style::new().bg(bg),
            },
            subscription: SubscriptionTheme {
                background: Style::new().bg(bg),
                header: Style::new().add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
                selected_feed: Style::new().fg(fg_focus).add_modifier(Modifier::BOLD),
            },
            entries: EntriesTheme {
                header: Style::new().add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
                selected_entry: Style::new().fg(fg_focus).add_modifier(Modifier::BOLD),
                summary: Style::new().fg(fg),
            },
            error: ErrorTheme {
                message: Style::new().fg(error).bg(bg),
            },
            default_icon_fg: fg,
            requirement: RequirementLabelTheme {
                must: bg,
                should: bg,
                may: bg,
                fg,
            },
            selection_popup: SelectionPopup {
                highlight: Style::new().bg(Color::Yellow).fg(bg),
            },
        }
    }
}

impl Default for Theme {
    fn default() -> Self {
        Theme::with_palette(Palette::ferra())
    }
}

impl Theme {
    pub(crate) fn contrast_fg_from_luminance(&self, luminance: f64) -> Color {
        if luminance > 0.5 {
            self.base.bg.unwrap_or_default()
        } else {
            self.base.fg.unwrap_or_default()
        }
    }
}