Skip to main content

streamdown_config/
style.rs

1//! Style configuration.
2//!
3//! This module contains the `StyleConfig` struct which holds
4//! all visual styling settings including HSV color multipliers.
5
6use serde::{Deserialize, Serialize};
7
8/// HSV multiplier for color transformations.
9///
10/// These multipliers are applied to base HSV values to create
11/// derived colors for different UI elements.
12#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
13#[serde(rename_all = "UPPERCASE")]
14pub struct HsvMultiplier {
15    /// Hue multiplier (typically 1.0 to preserve hue)
16    pub h: f64,
17    /// Saturation multiplier
18    pub s: f64,
19    /// Value (brightness) multiplier
20    pub v: f64,
21}
22
23impl Default for HsvMultiplier {
24    fn default() -> Self {
25        Self {
26            h: 1.0,
27            s: 1.0,
28            v: 1.0,
29        }
30    }
31}
32
33impl HsvMultiplier {
34    /// Create a new HSV multiplier.
35    pub fn new(h: f64, s: f64, v: f64) -> Self {
36        Self { h, s, v }
37    }
38
39    /// Create the "Dark" style multiplier (default values).
40    pub fn dark() -> Self {
41        Self::new(1.00, 1.50, 0.25)
42    }
43
44    /// Create the "Mid" style multiplier (default values).
45    pub fn mid() -> Self {
46        Self::new(1.00, 1.00, 0.50)
47    }
48
49    /// Create the "Symbol" style multiplier (default values).
50    pub fn symbol() -> Self {
51        Self::new(1.00, 1.00, 1.50)
52    }
53
54    /// Create the "Head" style multiplier (default values).
55    pub fn head() -> Self {
56        Self::new(1.00, 1.00, 1.75)
57    }
58
59    /// Create the "Grey" style multiplier (default values).
60    pub fn grey() -> Self {
61        Self::new(1.00, 0.25, 1.37)
62    }
63
64    /// Create the "Bright" style multiplier (default values).
65    pub fn bright() -> Self {
66        Self::new(1.00, 0.60, 2.00)
67    }
68}
69
70/// Style configuration.
71///
72/// Controls visual styling including margins, indentation,
73/// colors, and code block appearance.
74#[derive(Debug, Clone, Serialize, Deserialize)]
75#[serde(rename_all = "PascalCase")]
76pub struct StyleConfig {
77    /// Left margin in characters.
78    /// Default: 2
79    #[serde(default = "default_margin")]
80    pub margin: usize,
81
82    /// List item indentation in characters.
83    /// Default: 2
84    #[serde(default = "default_list_indent")]
85    pub list_indent: usize,
86
87    /// Enable pretty padding for code blocks.
88    /// Default: true
89    #[serde(default = "default_true")]
90    pub pretty_pad: bool,
91
92    /// Enable broken line indicators.
93    /// Default: true
94    #[serde(default = "default_true")]
95    pub pretty_broken: bool,
96
97    /// Terminal width override (0 = auto-detect).
98    /// Default: 0
99    #[serde(default)]
100    pub width: usize,
101
102    /// Base HSV color values [H, S, V].
103    /// H is in 0.0..1.0 range (will be scaled to 360)
104    /// S and V are in 0.0..1.0 range
105    /// Default: [0.8, 0.5, 0.5]
106    #[serde(default = "default_hsv", rename = "HSV")]
107    pub hsv: [f64; 3],
108
109    /// Dark color multiplier (for backgrounds).
110    #[serde(default = "HsvMultiplier::dark")]
111    pub dark: HsvMultiplier,
112
113    /// Mid color multiplier (for secondary elements).
114    #[serde(default = "HsvMultiplier::mid")]
115    pub mid: HsvMultiplier,
116
117    /// Symbol color multiplier (for special characters).
118    #[serde(default = "HsvMultiplier::symbol")]
119    pub symbol: HsvMultiplier,
120
121    /// Head color multiplier (for headers).
122    #[serde(default = "HsvMultiplier::head")]
123    pub head: HsvMultiplier,
124
125    /// Grey color multiplier (for muted text).
126    #[serde(default = "HsvMultiplier::grey")]
127    pub grey: HsvMultiplier,
128
129    /// Bright color multiplier (for emphasis).
130    #[serde(default = "HsvMultiplier::bright")]
131    pub bright: HsvMultiplier,
132
133    /// Syntax highlighting theme name.
134    /// Default: "native"
135    #[serde(default = "default_syntax")]
136    pub syntax: String,
137}
138
139impl Default for StyleConfig {
140    fn default() -> Self {
141        Self {
142            margin: 2,
143            list_indent: 2,
144            pretty_pad: true,
145            pretty_broken: true,
146            width: 0,
147            hsv: [0.8, 0.5, 0.5],
148            dark: HsvMultiplier::dark(),
149            mid: HsvMultiplier::mid(),
150            symbol: HsvMultiplier::symbol(),
151            head: HsvMultiplier::head(),
152            grey: HsvMultiplier::grey(),
153            bright: HsvMultiplier::bright(),
154            syntax: "native".to_string(),
155        }
156    }
157}
158
159impl StyleConfig {
160    /// Merge another StyleConfig into this one.
161    pub fn merge(&mut self, other: &StyleConfig) {
162        self.margin = other.margin;
163        self.list_indent = other.list_indent;
164        self.pretty_pad = other.pretty_pad;
165        self.pretty_broken = other.pretty_broken;
166        self.width = other.width;
167        self.hsv = other.hsv;
168        self.dark = other.dark;
169        self.mid = other.mid;
170        self.symbol = other.symbol;
171        self.head = other.head;
172        self.grey = other.grey;
173        self.bright = other.bright;
174        self.syntax.clone_from(&other.syntax);
175    }
176
177    /// Get the base HSV values as (H, S, V) tuple.
178    ///
179    /// H is scaled to 0..360 range for color calculations.
180    pub fn base_hsv(&self) -> (f64, f64, f64) {
181        // H in config is 0..1, convert to 0..360
182        (self.hsv[0] * 360.0, self.hsv[1], self.hsv[2])
183    }
184
185    /// Get effective width (auto-detect if 0).
186    pub fn effective_width(&self) -> usize {
187        if self.width == 0 {
188            // Try to get terminal width, fallback to 80
189            crossterm::terminal::size()
190                .map(|(w, _)| w as usize)
191                .unwrap_or(80)
192        } else {
193            self.width
194        }
195    }
196}
197
198fn default_margin() -> usize {
199    2
200}
201
202fn default_list_indent() -> usize {
203    2
204}
205
206fn default_true() -> bool {
207    true
208}
209
210fn default_hsv() -> [f64; 3] {
211    [0.8, 0.5, 0.5]
212}
213
214fn default_syntax() -> String {
215    "native".to_string()
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[test]
223    fn test_default_style() {
224        let style = StyleConfig::default();
225        assert_eq!(style.margin, 2);
226        assert_eq!(style.list_indent, 2);
227        assert!(style.pretty_pad);
228        assert!(style.pretty_broken);
229        assert_eq!(style.width, 0);
230        assert_eq!(style.hsv, [0.8, 0.5, 0.5]);
231        assert_eq!(style.syntax, "native");
232    }
233
234    #[test]
235    fn test_hsv_multiplier_defaults() {
236        let dark = HsvMultiplier::dark();
237        assert!((dark.h - 1.0).abs() < f64::EPSILON);
238        assert!((dark.s - 1.5).abs() < f64::EPSILON);
239        assert!((dark.v - 0.25).abs() < f64::EPSILON);
240
241        let bright = HsvMultiplier::bright();
242        assert!((bright.v - 2.0).abs() < f64::EPSILON);
243    }
244
245    #[test]
246    fn test_serde_pascal_case() {
247        let toml_str = r#"
248            Margin = 4
249            ListIndent = 3
250            PrettyPad = false
251            PrettyBroken = false
252            Width = 100
253            HSV = [0.5, 0.6, 0.7]
254            Dark = { H = 1.0, S = 2.0, V = 0.5 }
255            Syntax = "monokai"
256        "#;
257
258        let style: StyleConfig = toml::from_str(toml_str).unwrap();
259        assert_eq!(style.margin, 4);
260        assert_eq!(style.list_indent, 3);
261        assert!(!style.pretty_pad);
262        assert_eq!(style.width, 100);
263        assert_eq!(style.hsv, [0.5, 0.6, 0.7]);
264        assert!((style.dark.s - 2.0).abs() < f64::EPSILON);
265        assert_eq!(style.syntax, "monokai");
266    }
267
268    #[test]
269    fn test_base_hsv() {
270        let style = StyleConfig::default();
271        let (h, s, v) = style.base_hsv();
272        assert!((h - 288.0).abs() < f64::EPSILON); // 0.8 * 360
273        assert!((s - 0.5).abs() < f64::EPSILON);
274        assert!((v - 0.5).abs() < f64::EPSILON);
275    }
276
277    #[test]
278    fn test_merge() {
279        let mut base = StyleConfig::default();
280        let other = StyleConfig {
281            margin: 5,
282            ..Default::default()
283        };
284
285        base.merge(&other);
286        assert_eq!(base.margin, 5);
287    }
288}