Skip to main content

hjkl_theme/
style.rs

1use std::collections::HashMap;
2
3use serde::{Deserialize, Deserializer};
4
5use crate::{
6    ThemeError,
7    color::{Color, ColorRef},
8};
9
10// ---------------------------------------------------------------------------
11// UI surface styles
12// ---------------------------------------------------------------------------
13
14/// Resolved UI surface styles.
15#[derive(Clone, Default, Debug)]
16pub struct UiStyles {
17    pub background: Option<Color>,
18    pub foreground: Option<Color>,
19    pub cursor: Option<Color>,
20    pub cursorline: Option<Color>,
21    pub statusline: Option<StyleSpec>,
22    pub statusline_inactive: Option<StyleSpec>,
23    pub gutter: Option<Color>,
24    pub gutter_current: Option<Color>,
25    pub popup: Option<StyleSpec>,
26    pub selection: Option<StyleSpec>,
27    pub diagnostic_error: Option<Color>,
28    pub diagnostic_warn: Option<Color>,
29}
30
31/// Raw UI section from TOML before palette resolution.
32#[derive(Clone, Debug, Default, Deserialize)]
33pub(crate) struct RawUiStyles {
34    pub background: Option<ColorRef>,
35    pub foreground: Option<ColorRef>,
36    pub cursor: Option<ColorRef>,
37    pub cursorline: Option<ColorRef>,
38    pub statusline: Option<RawStyleSpec>,
39    #[serde(rename = "statusline.inactive")]
40    pub statusline_inactive: Option<RawStyleSpec>,
41    pub gutter: Option<ColorRef>,
42    #[serde(rename = "gutter.current")]
43    pub gutter_current: Option<ColorRef>,
44    pub popup: Option<RawStyleSpec>,
45    pub selection: Option<RawStyleSpec>,
46    #[serde(rename = "diagnostic.error")]
47    pub diagnostic_error: Option<ColorRef>,
48    #[serde(rename = "diagnostic.warn")]
49    pub diagnostic_warn: Option<ColorRef>,
50}
51
52impl RawUiStyles {
53    pub(crate) fn resolve(self, palette: &HashMap<String, Color>) -> Result<UiStyles, ThemeError> {
54        let resolve_color = |c: Option<ColorRef>| c.map(|cr| cr.resolve(palette)).transpose();
55        let resolve_style = |s: Option<RawStyleSpec>| s.map(|rs| rs.resolve(palette)).transpose();
56        Ok(UiStyles {
57            background: resolve_color(self.background)?,
58            foreground: resolve_color(self.foreground)?,
59            cursor: resolve_color(self.cursor)?,
60            cursorline: resolve_color(self.cursorline)?,
61            statusline: resolve_style(self.statusline)?,
62            statusline_inactive: resolve_style(self.statusline_inactive)?,
63            gutter: resolve_color(self.gutter)?,
64            gutter_current: resolve_color(self.gutter_current)?,
65            popup: resolve_style(self.popup)?,
66            selection: resolve_style(self.selection)?,
67            diagnostic_error: resolve_color(self.diagnostic_error)?,
68            diagnostic_warn: resolve_color(self.diagnostic_warn)?,
69        })
70    }
71}
72
73/// Per-character text modifiers.
74#[derive(Clone, Copy, Default, PartialEq, Eq, Debug)]
75pub struct Modifiers {
76    pub bold: bool,
77    pub italic: bool,
78    pub underline: bool,
79    pub reverse: bool,
80    pub strikethrough: bool,
81}
82
83/// Foreground, background, and modifier flags for a syntax or UI element.
84#[derive(Clone, Copy, Default, PartialEq, Eq, Debug)]
85pub struct StyleSpec {
86    pub fg: Option<Color>,
87    pub bg: Option<Color>,
88    pub modifiers: Modifiers,
89}
90
91// ---------------------------------------------------------------------------
92// Raw (unresolved) types used during TOML deserialization
93// ---------------------------------------------------------------------------
94
95/// Raw `modifiers` array from TOML — strings like `"bold"`, `"italic"`.
96#[derive(Clone, Debug, Default, Deserialize)]
97pub(crate) struct RawModifiers(pub Vec<String>);
98
99impl RawModifiers {
100    pub(crate) fn resolve(self) -> Result<Modifiers, ThemeError> {
101        let mut m = Modifiers::default();
102        for s in self.0 {
103            match s.as_str() {
104                "bold" => m.bold = true,
105                "italic" => m.italic = true,
106                "underline" => m.underline = true,
107                "reverse" => m.reverse = true,
108                "strikethrough" => m.strikethrough = true,
109                _ => return Err(ThemeError::BadModifier(s)),
110            }
111        }
112        Ok(m)
113    }
114}
115
116/// Full table form: `{ fg = "...", bg = "...", modifiers = [...] }`.
117#[derive(Clone, Debug, Deserialize)]
118pub(crate) struct RawStyleFull {
119    pub fg: Option<ColorRef>,
120    pub bg: Option<ColorRef>,
121    #[serde(default)]
122    pub modifiers: RawModifiers,
123}
124
125/// Raw `StyleSpec` before palette resolution.
126/// TOML shorthand `"#abc123"` or `"$name"` maps to `Shorthand`; table form maps to `Full`.
127#[derive(Clone, Debug)]
128pub(crate) enum RawStyleSpec {
129    Full(RawStyleFull),
130    Shorthand(ColorRef),
131}
132
133impl<'de> Deserialize<'de> for RawStyleSpec {
134    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
135        // Use toml::Value as the intermediate to branch on the TOML type cleanly.
136        let v = toml::Value::deserialize(d)?;
137        match v {
138            toml::Value::String(_) => {
139                let cr = ColorRef::deserialize(v).map_err(serde::de::Error::custom)?;
140                Ok(RawStyleSpec::Shorthand(cr))
141            }
142            toml::Value::Table(_) => {
143                let full = RawStyleFull::deserialize(v).map_err(serde::de::Error::custom)?;
144                Ok(RawStyleSpec::Full(full))
145            }
146            other => Err(serde::de::Error::custom(format!(
147                "expected string or table for style, got {}",
148                other.type_str()
149            ))),
150        }
151    }
152}
153
154impl RawStyleSpec {
155    pub(crate) fn resolve(self, palette: &HashMap<String, Color>) -> Result<StyleSpec, ThemeError> {
156        match self {
157            RawStyleSpec::Shorthand(cr) => Ok(StyleSpec {
158                fg: Some(cr.resolve(palette)?),
159                ..Default::default()
160            }),
161            RawStyleSpec::Full(f) => {
162                let fg = f.fg.map(|cr| cr.resolve(palette)).transpose()?;
163                let bg = f.bg.map(|cr| cr.resolve(palette)).transpose()?;
164                let modifiers = f.modifiers.resolve()?;
165                Ok(StyleSpec { fg, bg, modifiers })
166            }
167        }
168    }
169}