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}
27
28impl Default for Skin {
29 fn default() -> Self {
30 toml::from_str(include_str!("../assets/default.toml")).unwrap()
31 }
32}
33
34impl Skin {
35 pub(crate) fn from_file(file_path: &str) -> Result<Self, Box<dyn Error>> {
36 let f = shellexpand::env(file_path).map_or(file_path.to_string(), |x| x.to_string());
37
38 let toml_content = std::fs::read_to_string(f)?;
39
40 Ok(toml::from_str(&toml_content)?)
41 }
42
43 #[allow(clippy::too_many_lines)]
44 pub(crate) fn apply_to(&self, theme: &mut Theme) {
45 if let Some(target) = &self.base.focused {
47 set_fg_bg!(theme.base.focused, target, self.colors);
48 }
49 if let Some(target) = &self.base.unfocused {
50 set_fg_bg!(theme.base.unfocused, target, self.colors);
51 }
52
53 if let Some(target) = &self.highlight.focused {
55 set_fg_bg!(theme.highlight.focused, target, self.colors);
56 }
57 if let Some(target) = &self.highlight.unfocused {
58 set_fg_bg!(theme.highlight.unfocused, target, self.colors);
59 }
60
61 if let Some(target) = &self.border.unfocused {
63 if let Some(style) = &target.style {
64 set_fg_bg!(theme.border.unfocused, style, self.colors);
65 }
66 if let Some(border_type) = target.border_type {
67 theme.border.border_type_unfocused = border_type.into();
68 }
69 }
70 if let Some(target) = &self.border.focused {
71 if let Some(style) = &target.style {
72 set_fg_bg!(theme.border.focused, style, self.colors);
73 }
74 if let Some(border_type) = target.border_type {
75 theme.border.border_type_focused = border_type.into();
76 }
77 }
78
79 if let Some(target) = &self.title.focused {
81 set_fg_bg!(theme.title.focused, target, self.colors);
82 }
83 if let Some(target) = &self.title.unfocused {
84 set_fg_bg!(theme.title.unfocused, target, self.colors);
85 }
86 theme.title.focused = theme.title.focused.bold();
87 theme.title.unfocused = theme.title.unfocused.bold();
88
89 if let Some(hide_footer) = self.footer.hide {
91 theme.hide_footer = hide_footer;
92 } else {
93 theme.hide_footer = false;
94 }
95
96 if let Some(hide_status) = self.status.hide {
98 theme.hide_status = hide_status;
99 } else {
100 theme.hide_status = false;
101 }
102
103 if let Some(line_numbers) = self.editor.line_numbers {
105 theme.editor.line_numbers = line_numbers;
106 }
107 }
108}
109
110#[derive(Default, Debug, Deserialize)]
111pub(crate) struct Base {
112 pub focused: Option<FgBg>,
113 pub unfocused: Option<FgBg>,
114}
115
116#[derive(Default, Debug, Deserialize)]
117pub(crate) struct Highlight {
118 pub focused: Option<FgBg>,
119 pub unfocused: Option<FgBg>,
120}
121
122#[derive(Default, Debug, Deserialize)]
123pub(crate) struct Title {
124 pub focused: Option<FgBg>,
125 pub unfocused: Option<FgBg>,
126}
127
128#[derive(Debug, Deserialize, Default)]
129pub(crate) struct Border {
130 pub focused: Option<BorderConfig>,
131 pub unfocused: Option<BorderConfig>,
132}
133
134#[derive(Debug, Deserialize, Default)]
135pub(crate) struct BorderConfig {
136 #[serde(flatten)]
137 pub style: Option<FgBg>,
138 pub border_type: Option<SkinBorderType>,
139}
140
141#[derive(Debug, Clone, Copy, Deserialize)]
143#[serde(rename_all = "lowercase")]
144pub(crate) enum SkinBorderType {
145 Plain,
146 Rounded,
147 Double,
148 Thick,
149}
150
151impl From<SkinBorderType> for BorderType {
152 fn from(value: SkinBorderType) -> Self {
153 match value {
154 SkinBorderType::Plain => BorderType::Plain,
155 SkinBorderType::Rounded => BorderType::Rounded,
156 SkinBorderType::Double => BorderType::Double,
157 SkinBorderType::Thick => BorderType::Thick,
158 }
159 }
160}
161
162#[derive(Debug, Deserialize, Default)]
163pub(crate) struct Footer {
164 hide: Option<bool>,
165}
166
167#[derive(Debug, Deserialize, Default)]
168pub(crate) struct Status {
169 hide: Option<bool>,
170}
171
172#[derive(Debug, Deserialize, Default)]
173pub(crate) struct Editor {
174 pub line_numbers: Option<LineNumbers>,
175}
176
177#[derive(Debug, Deserialize, Default)]
178pub(crate) struct FgBg {
179 pub foreground: Option<String>,
180 pub background: Option<String>,
181}
182
183pub(crate) fn resolve_color(colors: &HashMap<String, Color>, color: Option<&str>) -> Option<Color> {
184 let color = color?;
185
186 if let Some(color) = colors.get(color) {
187 return Some(*color);
188 }
189
190 Color::from_str(color).ok()
191}
192
193pub(crate) mod macros {
194 #[macro_export]
195 macro_rules! set_fg_bg {
196 ($theme:expr, $fg_bg:expr, $colors:expr) => {
197 let fc = resolve_color(&$colors, $fg_bg.foreground.as_deref());
198 let bc = resolve_color(&$colors, $fg_bg.background.as_deref());
199 if let Some(fc) = fc {
200 $theme = $theme.fg(fc.0);
201 }
202 if let Some(bc) = bc {
203 $theme = $theme.bg(bc.0);
204 }
205 };
206 }
207}