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}