1use crate::{
2 ui::widgets::{PopupPosition, PopupSize},
3 utils::parse_color,
4};
5use ratatui::style::{Color, Style};
6use serde::Deserialize;
7
8#[derive(Deserialize, Debug, Clone, Copy)]
9pub struct ColorPair {
10 #[serde(default, deserialize_with = "deserialize_color_field")]
11 fg: Color,
12 #[serde(default, deserialize_with = "deserialize_color_field")]
13 bg: Color,
14
15 #[serde(default, deserialize_with = "deserialize_optional_color_field")]
16 selection_fg: Option<Color>,
17 #[serde(default, deserialize_with = "deserialize_optional_color_field")]
18 selection_bg: Option<Color>,
19}
20
21impl Default for ColorPair {
22 fn default() -> Self {
23 Self {
24 fg: Color::Reset,
25 bg: Color::Reset,
26 selection_fg: None,
27 selection_bg: None,
28 }
29 }
30}
31
32impl ColorPair {
33 pub fn as_style(&self) -> Style {
34 Style::default().fg(self.fg).bg(self.bg)
35 }
36
37 pub fn selection_style(&self, global_default: Style) -> Style {
38 let mut style = global_default;
39
40 if let Some(fg) = self.selection_fg {
41 style = style.fg(fg);
42 }
43
44 if let Some(bg) = self.selection_bg {
45 style = style.bg(bg);
46 }
47
48 style
49 }
50
51 pub fn effective_style(&self, fallback: &ColorPair) -> Style {
52 let fg = if self.fg == Color::Reset {
53 fallback.fg
54 } else {
55 self.fg
56 };
57 let bg = if self.bg == Color::Reset {
58 fallback.bg
59 } else {
60 self.bg
61 };
62 Style::default().fg(fg).bg(bg)
63 }
64
65 pub fn fg(&self) -> Color {
66 self.fg
67 }
68 pub fn bg(&self) -> Color {
69 self.bg
70 }
71}
72
73#[derive(Deserialize, Debug, Clone)]
74pub struct MarkerTheme {
75 #[serde(default)]
76 icon: String,
77 #[serde(flatten)]
78 color: ColorPair,
79}
80
81impl MarkerTheme {
82 pub fn icon(&self) -> &str {
83 &self.icon
84 }
85 pub fn color(&self) -> &ColorPair {
86 &self.color
87 }
88}
89
90impl Default for MarkerTheme {
91 fn default() -> Self {
92 MarkerTheme {
93 icon: "*".to_string(),
94 color: ColorPair {
95 fg: Color::Yellow,
96 bg: Color::Reset,
97 selection_fg: None,
98 selection_bg: None,
99 },
100 }
101 }
102}
103
104#[derive(Deserialize, Debug, Clone)]
105#[serde(default)]
106pub struct WidgetTheme {
107 #[serde(default)]
108 color: ColorPair,
109 #[serde(default)]
110 border: ColorPair,
111 #[serde(default)]
112 position: Option<PopupPosition>,
113 #[serde(default)]
114 size: Option<PopupSize>,
115 #[serde(default)]
116 confirm_size: Option<PopupSize>,
117}
118
119impl WidgetTheme {
120 pub fn position(&self) -> &Option<PopupPosition> {
121 &self.position
122 }
123
124 pub fn size(&self) -> &Option<PopupSize> {
125 &self.size
126 }
127
128 pub fn confirm_size(&self) -> &Option<PopupSize> {
129 &self.confirm_size
130 }
131
132 pub fn confirm_size_or(&self, fallback: PopupSize) -> PopupSize {
133 self.confirm_size()
134 .as_ref()
135 .or_else(|| self.size().as_ref())
136 .copied()
137 .unwrap_or(fallback)
138 }
139
140 pub fn border_or(&self, fallback: Style) -> Style {
141 if self.border.fg() == Color::Reset {
142 fallback
143 } else {
144 Style::default().fg(self.border.fg())
145 }
146 }
147
148 pub fn fg_or(&self, fallback: Style) -> Style {
149 if self.color.fg() == Color::Reset {
150 fallback
151 } else {
152 Style::default().fg(self.color.fg())
153 }
154 }
155
156 pub fn bg_or(&self, fallback: Style) -> Style {
157 if self.color.bg() == Color::Reset {
158 fallback
159 } else {
160 Style::default().bg(self.color.bg())
161 }
162 }
163}
164
165impl Default for WidgetTheme {
166 fn default() -> Self {
167 WidgetTheme {
168 color: ColorPair::default(),
169 border: ColorPair::default(),
170 position: Some(PopupPosition::Center),
171 size: Some(PopupSize::Medium),
172 confirm_size: Some(PopupSize::Large),
173 }
174 }
175}
176
177#[derive(Deserialize, Debug)]
178#[serde(default)]
179pub struct Theme {
180 selection: ColorPair,
181 underline: ColorPair,
182 accent: ColorPair,
183 entry: ColorPair,
184 directory: ColorPair,
185 separator: ColorPair,
186 selection_icon: String,
187 parent: ColorPair,
188 preview: ColorPair,
189 path: ColorPair,
190 marker: MarkerTheme,
191 widget: WidgetTheme,
192}
193
194impl Theme {
195 pub fn accent(&self) -> ColorPair {
196 self.accent
197 }
198
199 pub fn selection(&self) -> ColorPair {
200 self.selection
201 }
202
203 pub fn underline(&self) -> ColorPair {
204 self.underline
205 }
206
207 pub fn entry(&self) -> ColorPair {
208 self.entry
209 }
210
211 pub fn directory(&self) -> ColorPair {
212 self.directory
213 }
214
215 pub fn separator(&self) -> ColorPair {
216 self.separator
217 }
218
219 pub fn selection_icon(&self) -> &str {
220 &self.selection_icon
221 }
222
223 pub fn parent(&self) -> ColorPair {
224 self.parent
225 }
226
227 pub fn preview(&self) -> ColorPair {
228 self.preview
229 }
230
231 pub fn path(&self) -> ColorPair {
232 self.path
233 }
234
235 pub fn marker(&self) -> &MarkerTheme {
236 &self.marker
237 }
238
239 pub fn widget(&self) -> &WidgetTheme {
240 &self.widget
241 }
242}
243
244impl Default for Theme {
245 fn default() -> Self {
246 Theme {
247 accent: ColorPair::default(),
248 selection: ColorPair::default(),
249 underline: ColorPair::default(),
250 entry: ColorPair::default(),
251 directory: ColorPair {
252 fg: Color::Cyan,
253 ..ColorPair::default()
254 },
255 separator: ColorPair::default(),
256 selection_icon: ">".into(),
257 parent: ColorPair::default(),
258 preview: ColorPair::default(),
259 path: ColorPair {
260 fg: Color::Magenta,
261 ..ColorPair::default()
262 },
263 marker: MarkerTheme::default(),
264 widget: WidgetTheme::default(),
265 }
266 }
267}
268
269fn deserialize_color_field<'de, D>(deserializer: D) -> Result<Color, D::Error>
271where
272 D: serde::Deserializer<'de>,
273{
274 let s = String::deserialize(deserializer)?;
275 Ok(parse_color(&s))
276}
277
278fn deserialize_optional_color_field<'de, D>(deserializer: D) -> Result<Option<Color>, D::Error>
280where
281 D: serde::Deserializer<'de>,
282{
283 let s: Option<String> = Option::deserialize(deserializer)?;
284 match s {
285 Some(s) if s.to_lowercase() != "default" => Ok(Some(parse_color(&s))),
286 _ => Ok(None),
287 }
288}