runa_tui/config/
theme.rs

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
269// Helper function to deserialize Theme colors
270fn 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
278// Helper function to deserialize optinals Colors for Themes
279fn 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}