1use derive_builder::Builder;
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4use tiny_skia::{Color, GradientStop};
5
6use crate::{
7 snapshot::{ascii_snapshot::ASCIISnapshot, image_snapshot::ImageSnapshot},
8 themes::get_theme,
9 utils::color::RgbaColor,
10};
11
12pub const DEFAULT_WINDOW_MARGIN: f32 = 82.;
13
14#[derive(Clone, Serialize, Debug, JsonSchema)]
15#[serde(untagged)]
16pub enum DimensionValue {
17 Num(f32),
18 Max,
19}
20
21impl<'de> Deserialize<'de> for DimensionValue {
22 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
23 where
24 D: serde::Deserializer<'de>,
25 {
26 #[derive(Deserialize)]
27 #[serde(untagged)]
28 enum AnyType {
29 Num(f32),
30 Max(String),
31 }
32
33 Ok(match AnyType::deserialize(deserializer)? {
34 AnyType::Num(num) => DimensionValue::Num(num),
35 AnyType::Max(max) if max == "max" => DimensionValue::Max,
36 _ => {
37 return Err(serde::de::Error::custom(
38 "The value of DimensionValue should be a number or 'max'",
39 ))
40 }
41 })
42 }
43}
44
45#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
46pub struct Point<T> {
47 pub x: T,
48 pub y: T,
49}
50
51pub type GradientPoint = Point<DimensionValue>;
52
53impl Point<DimensionValue> {
54 pub fn into_f32_point(&self, pixmap_width: f32, pixmap_height: f32) -> Point<f32> {
55 let x = match self.x {
56 DimensionValue::Num(num) => num,
57 DimensionValue::Max => pixmap_width,
58 };
59 let y = match self.y {
60 DimensionValue::Num(num) => num,
61 DimensionValue::Max => pixmap_height,
62 };
63
64 Point { x, y }
65 }
66}
67
68#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
69pub struct LinearGradientStop {
70 position: f32,
71 color: String,
72}
73
74impl LinearGradientStop {
75 pub fn new(position: f32, color: &str) -> Self {
76 if position < 0. || position > 1. {
77 panic!("The position of the gradient stop should be in the range of 0.0 to 1.0");
78 }
79
80 LinearGradientStop {
81 position,
82 color: color.to_string(),
83 }
84 }
85}
86
87impl From<LinearGradientStop> for GradientStop {
88 fn from(stop: LinearGradientStop) -> Self {
89 let rgba_color: RgbaColor = stop.color.as_str().into();
90 let color: Color = rgba_color.into();
91
92 GradientStop::new(stop.position, color)
93 }
94}
95
96#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
97pub struct LinearGradient {
98 pub start: GradientPoint,
99 pub end: GradientPoint,
100 pub stops: Vec<LinearGradientStop>,
101}
102
103#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
104#[serde(untagged)]
105pub enum Background {
106 Solid(String),
107 Gradient(LinearGradient),
108}
109
110#[derive(Clone, Builder, Serialize, Deserialize, Debug, JsonSchema)]
111pub struct TitleConfig {
112 #[builder(setter(into, strip_option), default = String::from("CaskaydiaCove Nerd Font"))]
113 pub font_family: String,
114
115 #[builder(setter(into), default = String::from("#aca9b2"))]
116 pub color: String,
117}
118
119#[derive(Clone, Builder, Serialize, Deserialize, Debug, JsonSchema)]
120pub struct Margin {
121 #[builder(setter(into, strip_option), default = DEFAULT_WINDOW_MARGIN)]
122 pub x: f32,
123
124 #[builder(setter(into, strip_option), default = DEFAULT_WINDOW_MARGIN)]
125 pub y: f32,
126}
127
128#[derive(Clone, Builder, Serialize, Deserialize, Debug, JsonSchema, Default)]
129pub struct Breadcrumbs {
130 #[builder(default = false)]
131 pub enable: bool,
132
133 #[builder(setter(into, strip_option), default = String::from("/"))]
134 pub separator: String,
135
136 #[builder(setter(into, strip_option), default = String::from("CaskaydiaCove Nerd Font"))]
137 pub font_family: String,
138
139 #[builder(setter(into), default = String::from("#80848b"))]
140 pub color: String,
141}
142
143#[derive(Clone, Builder, Default, Serialize, Deserialize, Debug, JsonSchema)]
144pub struct Border {
145 #[builder(setter(into), default = String::from("#ffffff30"))]
146 pub color: String,
147
148 #[builder(setter(into), default = 1.)]
149 pub width: f32,
150}
151
152#[derive(Clone, Builder, Serialize, Deserialize, Debug, JsonSchema)]
153pub struct Shadow {
154 #[builder(default = 20.)]
155 pub radius: f32,
156
157 #[builder(setter(into), default = String::from("#0000004d"))]
158 pub color: String,
159}
160
161#[derive(Clone, Builder, Serialize, Deserialize, Debug, JsonSchema)]
162pub struct Window {
163 #[builder(setter(into), default = MarginBuilder::default().build().unwrap())]
164 pub margin: Margin,
165
166 #[builder(setter(into), default = TitleConfigBuilder::default().build().unwrap())]
167 pub title_config: TitleConfig,
168
169 #[builder(setter(into), default = BorderBuilder::default().build().unwrap())]
170 pub border: Border,
171
172 #[builder(default = true)]
173 pub mac_window_bar: bool,
174
175 #[builder(default = ShadowBuilder::default().build().unwrap())]
176 pub shadow: Shadow,
177}
178
179impl WindowBuilder {
180 pub fn from_window(window: Window) -> WindowBuilder {
181 WindowBuilder {
182 margin: Some(window.margin),
183 title_config: Some(window.title_config),
184 border: Some(window.border),
185 mac_window_bar: Some(window.mac_window_bar),
186 shadow: Some(window.shadow),
187 }
188 }
189}
190
191#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
192#[serde(untagged)]
193pub enum HighlightLine {
194 Single(u32, String),
195 Range(u32, u32, String),
196}
197
198#[derive(Clone, Builder, Serialize, Deserialize, Debug, JsonSchema)]
199pub struct CommandLineContent {
200 #[builder(setter(into))]
201 pub content: String,
202
203 #[builder(setter(into))]
204 pub full_command: String,
205}
206
207#[derive(Clone, Builder, Serialize, Deserialize, Debug, JsonSchema)]
208pub struct Code {
209 #[builder(setter(into))]
210 pub content: String,
211
212 #[builder(setter(into, strip_option), default = None)]
213 pub start_line_number: Option<u32>,
214
215 #[builder(setter(into), default = vec![])]
216 #[serde(default)]
217 pub highlight_lines: Vec<HighlightLine>,
218
219 #[builder(setter(into, strip_option), default = None)]
222 pub language: Option<String>,
223
224 #[builder(setter(into, strip_option), default = None)]
225 pub file_path: Option<String>,
226}
227
228#[derive(Clone, Builder, Serialize, Deserialize, Debug, JsonSchema, Default)]
229pub struct CommandOutputConfig {
230 #[builder(setter(into), default = String::from("❯"))]
231 pub prompt: String,
232
233 #[builder(setter(into), default = String::from("CaskaydiaCove Nerd Font"))]
234 pub font_family: String,
235
236 #[builder(setter(into), default = String::from("#F78FB3"))]
237 pub prompt_color: String,
238
239 #[builder(setter(into), default = String::from("#98C379"))]
240 pub command_color: String,
241
242 #[builder(setter(into), default = String::from("#ff0000"))]
243 pub string_arg_color: String,
244}
245
246#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
247#[serde(untagged)]
248pub enum Content {
249 Code(Code),
250 CommandOutput(Vec<CommandLineContent>),
251}
252
253#[derive(Clone, Builder, Serialize, Deserialize, Debug, JsonSchema, Default)]
254pub struct CodeConfig {
255 #[builder(setter(into), default = String::from("CaskaydiaCove Nerd Font"))]
259 pub font_family: String,
260
261 #[builder(setter(into, strip_option), default = BreadcrumbsBuilder::default().build().unwrap())]
269 #[serde(default)]
270 pub breadcrumbs: Breadcrumbs,
271}
272
273#[derive(Serialize, Deserialize, Clone, Builder, Debug, JsonSchema)]
276pub struct Watermark {
277 #[builder(setter(into))]
278 pub content: String,
279
280 #[builder(setter(into), default = String::from("Pacifico"))]
281 pub font_family: String,
282
283 #[builder(setter(into), default = String::from("#ffffff"))]
284 pub color: String,
285}
286
287impl WatermarkBuilder {
288 pub fn from_watermark(watermark: Option<Watermark>) -> WatermarkBuilder {
289 watermark
290 .and_then(|watermark| {
291 Some(WatermarkBuilder {
292 content: Some(watermark.content),
293 font_family: Some(watermark.font_family),
294 color: Some(watermark.color),
295 })
296 })
297 .unwrap_or(WatermarkBuilder::default())
298 }
299}
300
301#[derive(Clone, Builder, Serialize, Deserialize, Debug, JsonSchema)]
302#[builder(name = "CodeSnap", build_fn(validate = "Self::validate"))]
303#[builder(derive(serde::Deserialize, serde::Serialize, Debug, JsonSchema))]
304pub struct SnapshotConfig {
305 #[builder(setter(into, strip_option), default = WindowBuilder::default().build().unwrap())]
306 pub window: Window,
307
308 #[builder(setter(into), default = CommandOutputConfigBuilder::default().build().unwrap())]
310 pub command_output_config: CommandOutputConfig,
311
312 #[builder(setter(into), default = CodeConfigBuilder::default().build().unwrap())]
313 pub code_config: CodeConfig,
314
315 #[builder(setter(into), default = None)]
316 pub watermark: Option<Watermark>,
317
318 #[builder(setter(into))]
319 pub content: Content,
320
321 #[builder(default = 3)]
324 #[serde(default = "default_scale_factor")]
325 pub scale_factor: u8,
326
327 #[builder(setter(into, strip_option), default = vec![])]
333 pub themes_folders: Vec<String>,
334
335 #[builder(setter(into, strip_option), default = vec![])]
343 pub fonts_folders: Vec<String>,
344
345 #[builder(setter(into), default = String::from("candy"))]
352 pub theme: String,
353
354 #[builder(setter(into))]
355 pub background: Background,
356
357 #[builder(setter(into), default = String::from("#495162"))]
358 pub line_number_color: String,
359
360 #[builder(setter(into, strip_option), default = None)]
361 pub title: Option<String>,
362}
363
364impl CodeSnap {
365 fn validate(&self) -> Result<(), String> {
366 if let Some(scale_factor) = self.scale_factor {
367 if scale_factor < 1 {
368 return Err("The scale factor must be greater than 1".to_string());
369 }
370 }
371
372 Ok(())
373 }
374
375 pub fn from_default_theme() -> Result<CodeSnap, serde_json::Error> {
376 Self::from_theme("bamboo")
377 }
378
379 pub fn from_theme(theme_name: &str) -> Result<CodeSnap, serde_json::Error> {
380 let theme = get_theme(theme_name);
381
382 Self::from_config(&theme)
383 }
384
385 pub fn from_config(config: &str) -> Result<CodeSnap, serde_json::Error> {
386 serde_json::from_str::<CodeSnap>(config)
387 }
388
389 pub fn map_code_config<F>(&mut self, f: F) -> anyhow::Result<&mut Self>
390 where
391 F: Fn(CodeConfig) -> anyhow::Result<CodeConfig>,
392 {
393 self.code_config = Some(f(self
394 .code_config
395 .clone()
396 .unwrap_or(CodeConfigBuilder::default().build()?))?);
397
398 Ok(self)
399 }
400
401 pub fn map_code<F>(&mut self, f: F) -> anyhow::Result<&mut Self>
402 where
403 F: Fn(Code) -> anyhow::Result<Content>,
404 {
405 let content = self.content.clone().unwrap_or(Content::Code(
406 CodeBuilder::default().content(String::from("")).build()?,
407 ));
408 let code_content = match content {
409 Content::Code(code_content) => code_content,
410 _ => return Ok(self),
411 };
412
413 self.content = Some(f(code_content)?);
414
415 Ok(self)
416 }
417
418 pub fn map_window<F>(&mut self, f: F) -> anyhow::Result<&mut Self>
419 where
420 F: Fn(Window) -> anyhow::Result<Window>,
421 {
422 self.window = Some(f(self
423 .window
424 .clone()
425 .unwrap_or(WindowBuilder::default().build()?))?);
426
427 Ok(self)
428 }
429
430 pub fn map_watermark<F>(&mut self, f: F) -> anyhow::Result<&mut Self>
431 where
432 F: Fn(Option<Watermark>) -> anyhow::Result<Option<Watermark>>,
433 {
434 self.watermark = Some(f(self.watermark.clone().unwrap_or(None))?);
435
436 Ok(self)
437 }
438}
439
440impl SnapshotConfig {
441 pub fn create_snapshot(&self) -> anyhow::Result<ImageSnapshot> {
443 ImageSnapshot::from_config(self.clone())
444 }
445
446 pub fn create_ascii_snapshot(&self) -> anyhow::Result<ASCIISnapshot> {
459 ASCIISnapshot::from_config(self.clone())
460 }
461}
462
463fn default_scale_factor() -> u8 {
464 3
465}