synd_term/ui/
theme.rs

1use ratatui::style::{Color, Modifier, Style, Stylize};
2
3#[derive(Clone)]
4pub struct Theme {
5    pub name: &'static str,
6    pub base: Style,
7    pub application_title: Style,
8    pub login: LoginTheme,
9    pub tabs: Style,
10    pub tabs_selected: Style,
11    pub prompt: PromptTheme,
12    pub subscription: SubscriptionTheme,
13    pub entries: EntriesTheme,
14    pub error: ErrorTheme,
15    pub default_icon_fg: Color,
16    pub requirement: RequirementLabelTheme,
17    pub selection_popup: SelectionPopup,
18}
19
20#[derive(Clone)]
21pub struct LoginTheme {
22    pub title: Style,
23    pub selected_auth_provider_item: Style,
24}
25
26#[derive(Clone)]
27pub struct ErrorTheme {
28    pub message: Style,
29}
30
31#[derive(Clone)]
32pub struct PromptTheme {
33    pub key: Style,
34    pub key_desc: Style,
35    pub background: Style,
36}
37
38#[derive(Clone)]
39pub struct SubscriptionTheme {
40    pub background: Style,
41    pub header: Style,
42    pub selected_feed: Style,
43}
44
45#[derive(Clone)]
46pub struct EntriesTheme {
47    pub header: Style,
48    pub selected_entry: Style,
49    pub summary: Style,
50}
51
52#[derive(Clone)]
53pub struct RequirementLabelTheme {
54    pub must: Color,
55    pub should: Color,
56    pub may: Color,
57    pub fg: Color,
58}
59
60#[derive(Clone)]
61pub struct SelectionPopup {
62    pub highlight: Style,
63}
64
65#[derive(Clone, Debug)]
66pub struct Palette {
67    name: &'static str,
68    bg: Color,
69    fg: Color,
70    fg_inactive: Color,
71    fg_focus: Color,
72    error: Color,
73}
74
75impl Palette {
76    pub fn dracula() -> Self {
77        Self {
78            name: "dracula",
79            bg: Color::Rgb(0x28, 0x2a, 0x36),
80            fg: Color::Rgb(0xf8, 0xf8, 0xf2),
81            fg_inactive: Color::Rgb(0x62, 0x72, 0xa4),
82            fg_focus: Color::Rgb(0xff, 0x79, 0xc6),
83            error: Color::Rgb(0xff, 0x55, 0x55),
84        }
85    }
86
87    pub fn eldritch() -> Self {
88        Self {
89            name: "eldritch",
90            bg: Color::Rgb(0x21, 0x23, 0x37),
91            fg: Color::Rgb(0xeb, 0xfa, 0xfa),
92            fg_inactive: Color::Rgb(0x70, 0x81, 0xd0),
93            fg_focus: Color::Rgb(0x37, 0xf4, 0x99),
94            error: Color::Rgb(0xf1, 0x6c, 0x75),
95        }
96    }
97
98    pub fn helix() -> Self {
99        Self {
100            name: "helix",
101            bg: Color::Rgb(0x3b, 0x22, 0x4c),
102            fg: Color::Rgb(0xa4, 0xa0, 0xe8),
103            fg_inactive: Color::Rgb(0x69, 0x7c, 0x81),
104            fg_focus: Color::Rgb(0xff, 0xff, 0xff),
105            error: Color::Rgb(0xf4, 0x78, 0x68),
106        }
107    }
108
109    pub fn ferra() -> Self {
110        Self {
111            name: "ferra",
112            bg: Color::Rgb(0x2b, 0x29, 0x2d),
113            fg: Color::Rgb(0xfe, 0xcd, 0xb2),
114            fg_inactive: Color::Rgb(0x6F, 0x5D, 0x63),
115            fg_focus: Color::Rgb(0xff, 0xa0, 0x7a),
116            error: Color::Rgb(0xe0, 0x6b, 0x75),
117        }
118    }
119
120    pub fn solarized_dark() -> Self {
121        Self {
122            name: "solarized_dark",
123            bg: Color::Rgb(0x00, 0x2b, 0x36),
124            fg: Color::Rgb(0x93, 0xa1, 0xa1),
125            fg_inactive: Color::Rgb(0x58, 0x6e, 0x75),
126            fg_focus: Color::Rgb(0x26, 0x8b, 0xd2),
127            error: Color::Rgb(0xdc, 0x32, 0x2f),
128        }
129    }
130}
131
132impl Theme {
133    #[allow(clippy::needless_pass_by_value)]
134    pub fn with_palette(p: Palette) -> Self {
135        let Palette {
136            name,
137            bg,
138            fg,
139            fg_inactive,
140            fg_focus,
141            error,
142        } = p;
143
144        Self {
145            name,
146            base: Style::new().bg(bg).fg(fg),
147            application_title: Style::new().fg(fg).bg(bg).add_modifier(Modifier::BOLD),
148            login: LoginTheme {
149                title: Style::new().add_modifier(Modifier::BOLD),
150                selected_auth_provider_item: Style::new().add_modifier(Modifier::BOLD),
151            },
152            tabs: Style::new().fg(fg),
153            tabs_selected: Style::new().fg(fg_focus).bold(),
154            prompt: PromptTheme {
155                key: Style::new().fg(fg_inactive).bg(bg),
156                key_desc: Style::new().fg(fg_inactive).bg(bg),
157                background: Style::new().bg(bg),
158            },
159            subscription: SubscriptionTheme {
160                background: Style::new().bg(bg),
161                header: Style::new().add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
162                selected_feed: Style::new().fg(fg_focus).add_modifier(Modifier::BOLD),
163            },
164            entries: EntriesTheme {
165                header: Style::new().add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
166                selected_entry: Style::new().fg(fg_focus).add_modifier(Modifier::BOLD),
167                summary: Style::new().fg(fg),
168            },
169            error: ErrorTheme {
170                message: Style::new().fg(error).bg(bg),
171            },
172            default_icon_fg: fg,
173            requirement: RequirementLabelTheme {
174                must: bg,
175                should: bg,
176                may: bg,
177                fg,
178            },
179            selection_popup: SelectionPopup {
180                highlight: Style::new().bg(Color::Yellow).fg(bg),
181            },
182        }
183    }
184}
185
186impl Default for Theme {
187    fn default() -> Self {
188        Theme::with_palette(Palette::ferra())
189    }
190}
191
192impl Theme {
193    pub(crate) fn contrast_fg_from_luminance(&self, luminance: f64) -> Color {
194        if luminance > 0.5 {
195            self.base.bg.unwrap_or_default()
196        } else {
197            self.base.fg.unwrap_or_default()
198        }
199    }
200}