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}