Skip to main content

cryosnap_core/
config.rs

1use serde::{Deserialize, Serialize};
2
3use crate::{
4    DEFAULT_PNG_OPT_LEVEL, DEFAULT_PNG_QUANTIZE_DITHER, DEFAULT_PNG_QUANTIZE_QUALITY,
5    DEFAULT_PNG_QUANTIZE_SPEED, DEFAULT_RASTER_MAX_PIXELS, DEFAULT_RASTER_SCALE,
6    DEFAULT_TITLE_MAX_WIDTH, DEFAULT_TITLE_OPACITY, DEFAULT_TITLE_SIZE,
7};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10#[serde(default)]
11pub struct Config {
12    pub theme: String,
13    pub background: String,
14    #[serde(deserialize_with = "deserialize_box")]
15    pub padding: Vec<f32>,
16    #[serde(deserialize_with = "deserialize_box")]
17    pub margin: Vec<f32>,
18    pub width: f32,
19    pub height: f32,
20    #[serde(rename = "window")]
21    pub window_controls: bool,
22    #[serde(rename = "show_line_numbers")]
23    pub show_line_numbers: bool,
24    pub language: Option<String>,
25    pub execute_timeout_ms: u64,
26    pub wrap: usize,
27    #[serde(deserialize_with = "deserialize_lines")]
28    pub lines: Vec<i32>,
29    pub border: Border,
30    pub shadow: Shadow,
31    pub font: Font,
32    #[serde(rename = "line_height")]
33    pub line_height: f32,
34    pub raster: RasterOptions,
35    pub png: PngOptions,
36    pub title: TitleOptions,
37}
38
39impl Default for Config {
40    fn default() -> Self {
41        Self {
42            theme: "charm".to_string(),
43            background: "#171717".to_string(),
44            padding: vec![20.0, 40.0, 20.0, 20.0],
45            margin: vec![0.0],
46            width: 0.0,
47            height: 0.0,
48            window_controls: false,
49            show_line_numbers: false,
50            language: None,
51            execute_timeout_ms: 10_000,
52            wrap: 0,
53            lines: vec![0, -1],
54            border: Border::default(),
55            shadow: Shadow::default(),
56            font: Font::default(),
57            line_height: 1.2,
58            raster: RasterOptions::default(),
59            png: PngOptions::default(),
60            title: TitleOptions::default(),
61        }
62    }
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
66#[serde(default)]
67pub struct Border {
68    pub radius: f32,
69    pub width: f32,
70    pub color: String,
71}
72
73impl Default for Border {
74    fn default() -> Self {
75        Self {
76            radius: 0.0,
77            width: 0.0,
78            color: "#515151".to_string(),
79        }
80    }
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
84#[serde(default)]
85pub struct Shadow {
86    pub blur: f32,
87    pub x: f32,
88    pub y: f32,
89}
90
91impl Default for Shadow {
92    fn default() -> Self {
93        Self {
94            blur: 0.0,
95            x: 0.0,
96            y: 0.0,
97        }
98    }
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
102#[serde(default)]
103pub struct Font {
104    pub family: String,
105    pub file: Option<String>,
106    pub size: f32,
107    pub ligatures: bool,
108    pub fallbacks: Vec<String>,
109    #[serde(rename = "system_fallback")]
110    pub system_fallback: FontSystemFallback,
111    #[serde(rename = "auto_download")]
112    pub auto_download: bool,
113    #[serde(rename = "force_update")]
114    pub force_update: bool,
115    #[serde(rename = "cjk_region")]
116    pub cjk_region: CjkRegion,
117    #[serde(rename = "dirs")]
118    pub dirs: Vec<String>,
119}
120
121impl Default for Font {
122    fn default() -> Self {
123        Self {
124            family: "monospace".to_string(),
125            file: None,
126            size: 14.0,
127            ligatures: true,
128            fallbacks: Vec::new(),
129            system_fallback: FontSystemFallback::default(),
130            auto_download: true,
131            force_update: false,
132            cjk_region: CjkRegion::default(),
133            dirs: Vec::new(),
134        }
135    }
136}
137
138#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
139#[serde(rename_all = "lowercase")]
140pub enum FontSystemFallback {
141    #[default]
142    Auto,
143    Always,
144    Never,
145}
146
147#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq, Hash)]
148#[serde(rename_all = "lowercase")]
149pub enum CjkRegion {
150    #[default]
151    Auto,
152    Sc,
153    Tc,
154    Hk,
155    Jp,
156    Kr,
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
160#[serde(default)]
161pub struct RasterOptions {
162    pub scale: f32,
163    pub max_pixels: u64,
164    pub backend: RasterBackend,
165}
166
167impl Default for RasterOptions {
168    fn default() -> Self {
169        Self {
170            scale: DEFAULT_RASTER_SCALE,
171            max_pixels: DEFAULT_RASTER_MAX_PIXELS,
172            backend: RasterBackend::Auto,
173        }
174    }
175}
176
177#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
178#[serde(rename_all = "lowercase")]
179pub enum RasterBackend {
180    #[default]
181    Auto,
182    Resvg,
183    Rsvg,
184}
185
186#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
187#[serde(rename_all = "lowercase")]
188pub enum TitleAlign {
189    Left,
190    #[default]
191    Center,
192    Right,
193}
194
195#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
196#[serde(rename_all = "lowercase")]
197pub enum TitlePathStyle {
198    #[default]
199    Absolute,
200    Relative,
201    Basename,
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
205#[serde(default)]
206pub struct TitleOptions {
207    pub enabled: bool,
208    pub text: Option<String>,
209    pub path_style: TitlePathStyle,
210    pub tmux_format: String,
211    pub align: TitleAlign,
212    pub size: f32,
213    pub color: String,
214    pub opacity: f32,
215    pub max_width: usize,
216    pub ellipsis: String,
217}
218
219impl Default for TitleOptions {
220    fn default() -> Self {
221        Self {
222            enabled: true,
223            text: None,
224            path_style: TitlePathStyle::Absolute,
225            tmux_format: "#{session_name}:#{window_index}.#{pane_index} #{pane_title}".to_string(),
226            align: TitleAlign::Center,
227            size: DEFAULT_TITLE_SIZE,
228            color: "#C5C8C6".to_string(),
229            opacity: DEFAULT_TITLE_OPACITY,
230            max_width: DEFAULT_TITLE_MAX_WIDTH,
231            ellipsis: "…".to_string(),
232        }
233    }
234}
235
236#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
237#[serde(rename_all = "lowercase")]
238pub enum PngStrip {
239    None,
240    #[default]
241    Safe,
242    All,
243}
244
245#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
246#[serde(rename_all = "lowercase")]
247pub enum PngQuantPreset {
248    Fast,
249    Balanced,
250    Best,
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize)]
254#[serde(default)]
255pub struct PngOptions {
256    pub optimize: bool,
257    pub level: u8,
258    pub strip: PngStrip,
259    pub quantize: bool,
260    pub quantize_preset: Option<PngQuantPreset>,
261    pub quantize_quality: u8,
262    pub quantize_speed: u8,
263    pub quantize_dither: f32,
264}
265
266impl Default for PngOptions {
267    fn default() -> Self {
268        Self {
269            optimize: true,
270            level: DEFAULT_PNG_OPT_LEVEL,
271            strip: PngStrip::Safe,
272            quantize: false,
273            quantize_preset: None,
274            quantize_quality: DEFAULT_PNG_QUANTIZE_QUALITY,
275            quantize_speed: DEFAULT_PNG_QUANTIZE_SPEED,
276            quantize_dither: DEFAULT_PNG_QUANTIZE_DITHER,
277        }
278    }
279}
280
281fn deserialize_box<'de, D>(deserializer: D) -> std::result::Result<Vec<f32>, D::Error>
282where
283    D: serde::Deserializer<'de>,
284{
285    let value = serde_json::Value::deserialize(deserializer)?;
286    parse_box_value(&value).map_err(serde::de::Error::custom)
287}
288
289fn parse_box_value(value: &serde_json::Value) -> std::result::Result<Vec<f32>, String> {
290    match value {
291        serde_json::Value::Number(n) => n
292            .as_f64()
293            .map(|v| vec![v as f32])
294            .ok_or_else(|| "invalid number".to_string()),
295        serde_json::Value::String(s) => parse_box_string(s),
296        serde_json::Value::Array(arr) => {
297            let mut out = Vec::new();
298            for item in arr {
299                match item {
300                    serde_json::Value::Number(n) => {
301                        out.push(n.as_f64().ok_or_else(|| "invalid number".to_string())? as f32);
302                    }
303                    serde_json::Value::String(s) => {
304                        let parsed = parse_box_string(s)?;
305                        out.extend(parsed);
306                    }
307                    _ => return Err("invalid array value".to_string()),
308                }
309            }
310            if matches!(out.len(), 1 | 2 | 4) {
311                Ok(out)
312            } else {
313                Err(format!("expected 1, 2, or 4 values, got {}", out.len()))
314            }
315        }
316        serde_json::Value::Null => Ok(vec![0.0]),
317        _ => Err("invalid box value".to_string()),
318    }
319}
320
321fn parse_box_string(input: &str) -> std::result::Result<Vec<f32>, String> {
322    let parts: Vec<&str> = input.split([',', ' ']).filter(|s| !s.is_empty()).collect();
323    if parts.is_empty() {
324        return Ok(vec![0.0]);
325    }
326    let mut out = Vec::new();
327    for part in parts {
328        let value = part
329            .parse::<f32>()
330            .map_err(|_| format!("invalid number {}", part))?;
331        out.push(value);
332    }
333    if matches!(out.len(), 1 | 2 | 4) {
334        Ok(out)
335    } else {
336        Err(format!("expected 1, 2, or 4 values, got {}", out.len()))
337    }
338}
339
340fn deserialize_lines<'de, D>(deserializer: D) -> std::result::Result<Vec<i32>, D::Error>
341where
342    D: serde::Deserializer<'de>,
343{
344    let value = serde_json::Value::deserialize(deserializer)?;
345    parse_lines_value(&value).map_err(serde::de::Error::custom)
346}
347
348fn parse_lines_value(value: &serde_json::Value) -> std::result::Result<Vec<i32>, String> {
349    match value {
350        serde_json::Value::Number(n) => n
351            .as_i64()
352            .map(|v| vec![v as i32])
353            .ok_or_else(|| "invalid number".to_string()),
354        serde_json::Value::String(s) => parse_lines_string(s),
355        serde_json::Value::Array(arr) => {
356            let mut out = Vec::new();
357            for item in arr {
358                match item {
359                    serde_json::Value::Number(n) => {
360                        out.push(n.as_i64().ok_or_else(|| "invalid number".to_string())? as i32);
361                    }
362                    serde_json::Value::String(s) => {
363                        let parsed = parse_lines_string(s)?;
364                        out.extend(parsed);
365                    }
366                    _ => return Err("invalid array value".to_string()),
367                }
368            }
369            if matches!(out.len(), 1 | 2) {
370                Ok(out)
371            } else {
372                Err(format!("expected 1 or 2 values, got {}", out.len()))
373            }
374        }
375        serde_json::Value::Null => Ok(vec![]),
376        _ => Err("invalid lines value".to_string()),
377    }
378}
379
380fn parse_lines_string(input: &str) -> std::result::Result<Vec<i32>, String> {
381    let parts: Vec<&str> = input.split([',', ' ']).filter(|s| !s.is_empty()).collect();
382    if parts.is_empty() {
383        return Ok(vec![]);
384    }
385    let mut out = Vec::new();
386    for part in parts {
387        let value = part
388            .parse::<i32>()
389            .map_err(|_| format!("invalid number {}", part))?;
390        out.push(value);
391    }
392    if matches!(out.len(), 1 | 2) {
393        Ok(out)
394    } else {
395        Err(format!("expected 1 or 2 values, got {}", out.len()))
396    }
397}