1use std::{collections::HashMap, error::Error, str::FromStr};
2
3use ratatui::widgets::BorderType;
4use serde::Deserialize;
5
6use crate::{color::Color, set_fg_bg, theme::LineNumbers, Theme};
7
8#[derive(Debug, Deserialize)]
9pub struct Skin {
10 #[serde(default)]
11 pub colors: HashMap<String, Color>,
12 #[serde(default)]
13 pub base: Base,
14 #[serde(default)]
15 pub title: Title,
16 #[serde(default)]
17 pub highlight: Highlight,
18 #[serde(default)]
19 pub border: Border,
20 #[serde(default)]
21 pub footer: Footer,
22 #[serde(default)]
23 pub status: Status,
24 #[serde(default)]
25 pub editor: Editor,
26 #[serde(default)]
27 pub layout: Layout,
28}
29
30impl Default for Skin {
31 fn default() -> Self {
32 toml::from_str(include_str!("../assets/default.toml")).unwrap()
33 }
34}
35
36impl Skin {
37 pub(crate) fn from_file(file_path: &str) -> Result<Self, Box<dyn Error>> {
38 let f = shellexpand::env(file_path).map_or(file_path.to_string(), |x| x.to_string());
39
40 let toml_content = std::fs::read_to_string(f)?;
41
42 Ok(toml::from_str(&toml_content)?)
43 }
44
45 #[allow(clippy::too_many_lines)]
46 pub(crate) fn apply_to(&self, theme: &mut Theme) {
47 if let Some(main_split) = &self.layout.main_split {
49 theme.layout.main_split = match main_split {
50 SplitDirection::Vertical => ratatui::layout::Direction::Vertical,
51 SplitDirection::Horizontal => ratatui::layout::Direction::Horizontal,
52 }
53 }
54
55 if let Some(target) = &self.base.focused {
57 set_fg_bg!(theme.base.focused, target, self.colors);
58 }
59 if let Some(target) = &self.base.unfocused {
60 set_fg_bg!(theme.base.unfocused, target, self.colors);
61 }
62
63 if let Some(target) = &self.highlight.focused {
65 set_fg_bg!(theme.highlight.focused, target, self.colors);
66 }
67 if let Some(target) = &self.highlight.unfocused {
68 set_fg_bg!(theme.highlight.unfocused, target, self.colors);
69 }
70
71 if let Some(target) = &self.border.unfocused {
73 if let Some(style) = &target.style {
74 set_fg_bg!(theme.border.unfocused, style, self.colors);
75 }
76 if let Some(border_type) = target.border_type {
77 theme.border.border_type_unfocused = border_type.into();
78 }
79 }
80 if let Some(target) = &self.border.focused {
81 if let Some(style) = &target.style {
82 set_fg_bg!(theme.border.focused, style, self.colors);
83 }
84 if let Some(border_type) = target.border_type {
85 theme.border.border_type_focused = border_type.into();
86 }
87 }
88
89 if let Some(target) = &self.title.focused {
91 set_fg_bg!(theme.title.focused, target, self.colors);
92 }
93 if let Some(target) = &self.title.unfocused {
94 set_fg_bg!(theme.title.unfocused, target, self.colors);
95 }
96 theme.title.focused = theme.title.focused.bold();
97 theme.title.unfocused = theme.title.unfocused.bold();
98
99 if let Some(hide_footer) = self.footer.hide {
101 theme.hide_footer = hide_footer;
102 } else {
103 theme.hide_footer = false;
104 }
105
106 if let Some(hide_status) = self.status.hide {
108 theme.hide_status = hide_status;
109 } else {
110 theme.hide_status = false;
111 }
112
113 if let Some(line_numbers) = self.editor.line_numbers {
115 theme.editor.line_numbers = line_numbers;
116 }
117 }
118}
119
120#[derive(Default, Debug, Deserialize)]
121pub(crate) struct Base {
122 pub focused: Option<FgBg>,
123 pub unfocused: Option<FgBg>,
124}
125
126#[derive(Default, Debug, Deserialize)]
127pub(crate) struct Highlight {
128 pub focused: Option<FgBg>,
129 pub unfocused: Option<FgBg>,
130}
131
132#[derive(Default, Debug, Deserialize)]
133pub(crate) struct Title {
134 pub focused: Option<FgBg>,
135 pub unfocused: Option<FgBg>,
136}
137
138#[derive(Debug, Deserialize, Default)]
139pub(crate) struct Border {
140 pub focused: Option<BorderConfig>,
141 pub unfocused: Option<BorderConfig>,
142}
143
144#[derive(Debug, Deserialize, Default)]
145pub(crate) struct BorderConfig {
146 #[serde(flatten)]
147 pub style: Option<FgBg>,
148 pub border_type: Option<SkinBorderType>,
149}
150
151#[derive(Debug, Clone, Copy, Deserialize)]
153#[serde(rename_all = "lowercase")]
154pub(crate) enum SkinBorderType {
155 Plain,
156 Rounded,
157 Double,
158 Thick,
159}
160
161impl From<SkinBorderType> for BorderType {
162 fn from(value: SkinBorderType) -> Self {
163 match value {
164 SkinBorderType::Plain => BorderType::Plain,
165 SkinBorderType::Rounded => BorderType::Rounded,
166 SkinBorderType::Double => BorderType::Double,
167 SkinBorderType::Thick => BorderType::Thick,
168 }
169 }
170}
171
172#[derive(Debug, Deserialize, Default)]
173pub(crate) struct Footer {
174 hide: Option<bool>,
175}
176
177#[derive(Debug, Deserialize, Default)]
178pub(crate) struct Status {
179 hide: Option<bool>,
180}
181
182#[derive(Debug, Deserialize, Default)]
183pub(crate) struct Editor {
184 pub line_numbers: Option<LineNumbers>,
185}
186
187#[derive(Debug, Deserialize, Default)]
188pub(crate) struct FgBg {
189 pub foreground: Option<String>,
190 pub background: Option<String>,
191}
192
193#[derive(Debug, Deserialize, Default)]
194pub(crate) struct Layout {
195 pub main_split: Option<SplitDirection>,
196}
197
198#[derive(Default, Debug, Deserialize, Clone, Copy)]
199#[serde(rename_all = "lowercase")]
200pub(crate) enum SplitDirection {
201 #[default]
202 Vertical,
203 Horizontal,
204}
205
206pub(crate) fn resolve_color(colors: &HashMap<String, Color>, color: Option<&str>) -> Option<Color> {
207 let color = color?;
208
209 if let Some(color) = colors.get(color) {
210 return Some(*color);
211 }
212
213 Color::from_str(color).ok()
214}
215
216pub(crate) mod macros {
217 #[macro_export]
218 macro_rules! set_fg_bg {
219 ($theme:expr, $fg_bg:expr, $colors:expr) => {
220 let fc = resolve_color(&$colors, $fg_bg.foreground.as_deref());
221 let bc = resolve_color(&$colors, $fg_bg.background.as_deref());
222 if let Some(fc) = fc {
223 $theme = $theme.fg(fc.0);
224 }
225 if let Some(bc) = bc {
226 $theme = $theme.bg(bc.0);
227 }
228 };
229 }
230}