wireman_theme/
skin.rs

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        // Layout
48        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        // Base
56        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        // Highlight
64        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        // Border
72        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        // Title
90        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        // Footer
100        if let Some(hide_footer) = self.footer.hide {
101            theme.hide_footer = hide_footer;
102        } else {
103            theme.hide_footer = false;
104        }
105
106        // Status
107        if let Some(hide_status) = self.status.hide {
108            theme.hide_status = hide_status;
109        } else {
110            theme.hide_status = false;
111        }
112
113        // Editor
114        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/// Border type configuration that can be deserialized from skin files.
152#[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}