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 #[builder(default = 12.0)]
179 pub radius: f32,
180}
181
182impl WindowBuilder {
183 pub fn from_window(window: Window) -> WindowBuilder {
184 WindowBuilder {
185 margin: Some(window.margin),
186 title_config: Some(window.title_config),
187 border: Some(window.border),
188 mac_window_bar: Some(window.mac_window_bar),
189 shadow: Some(window.shadow),
190 radius: Some(window.radius),
191 }
192 }
193}
194
195#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
196#[serde(untagged)]
197pub enum HighlightLine {
198 Single(u32, String),
199 Range(u32, u32, String),
200}
201
202#[derive(Clone, Builder, Serialize, Deserialize, Debug, JsonSchema)]
203pub struct CommandLineContent {
204 #[builder(setter(into))]
205 pub content: String,
206
207 #[builder(setter(into))]
208 pub full_command: String,
209}
210
211#[derive(Clone, Builder, Serialize, Deserialize, Debug, JsonSchema)]
212pub struct Code {
213 #[builder(setter(into))]
214 pub content: String,
215
216 #[builder(setter(into, strip_option), default = None)]
217 pub start_line_number: Option<u32>,
218
219 #[builder(setter(into), default = vec![])]
220 #[serde(default)]
221 pub highlight_lines: Vec<HighlightLine>,
222
223 #[builder(setter(into, strip_option), default = None)]
226 pub language: Option<String>,
227
228 #[builder(setter(into, strip_option), default = None)]
229 pub file_path: Option<String>,
230}
231
232#[derive(Clone, Builder, Serialize, Deserialize, Debug, JsonSchema, Default)]
233pub struct CommandOutputConfig {
234 #[builder(setter(into), default = String::from("❯"))]
235 pub prompt: String,
236
237 #[builder(setter(into), default = String::from("CaskaydiaCove Nerd Font"))]
238 pub font_family: String,
239
240 #[builder(setter(into), default = String::from("#F78FB3"))]
241 pub prompt_color: String,
242
243 #[builder(setter(into), default = String::from("#98C379"))]
244 pub command_color: String,
245
246 #[builder(setter(into), default = String::from("#ff0000"))]
247 pub string_arg_color: String,
248}
249
250#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
251#[serde(untagged)]
252pub enum Content {
253 Code(Code),
254 CommandOutput(Vec<CommandLineContent>),
255 Image(Vec<u8>),
256}
257
258#[derive(Clone, Builder, Serialize, Deserialize, Debug, JsonSchema, Default)]
259pub struct CodeConfig {
260 #[builder(setter(into), default = String::from("CaskaydiaCove Nerd Font"))]
264 pub font_family: String,
265
266 #[builder(setter(into, strip_option), default = BreadcrumbsBuilder::default().build().unwrap())]
274 #[serde(default)]
275 pub breadcrumbs: Breadcrumbs,
276}
277
278#[derive(Serialize, Deserialize, Clone, Builder, Debug, JsonSchema)]
281pub struct Watermark {
282 #[builder(setter(into))]
283 pub content: String,
284
285 #[builder(setter(into), default = String::from("Pacifico"))]
286 pub font_family: String,
287
288 #[builder(setter(into), default = String::from("#ffffff"))]
289 pub color: String,
290}
291
292impl WatermarkBuilder {
293 pub fn from_watermark(watermark: Option<Watermark>) -> WatermarkBuilder {
294 watermark
295 .and_then(|watermark| {
296 Some(WatermarkBuilder {
297 content: Some(watermark.content),
298 font_family: Some(watermark.font_family),
299 color: Some(watermark.color),
300 })
301 })
302 .unwrap_or(WatermarkBuilder::default())
303 }
304}
305
306#[derive(Clone, Builder, Serialize, Deserialize, Debug, JsonSchema)]
307#[builder(name = "CodeSnap", build_fn(validate = "Self::validate"))]
308#[builder(derive(serde::Deserialize, serde::Serialize, Debug, JsonSchema))]
309pub struct SnapshotConfig {
310 #[builder(setter(into, strip_option), default = WindowBuilder::default().build().unwrap())]
311 pub window: Window,
312
313 #[builder(setter(into), default = CommandOutputConfigBuilder::default().build().unwrap())]
315 pub command_output_config: CommandOutputConfig,
316
317 #[builder(setter(into), default = CodeConfigBuilder::default().build().unwrap())]
318 pub code_config: CodeConfig,
319
320 #[builder(setter(into), default = None)]
321 pub watermark: Option<Watermark>,
322
323 #[builder(setter(into))]
324 pub content: Content,
325
326 #[builder(default = 3)]
329 #[serde(default = "default_scale_factor")]
330 pub scale_factor: u8,
331
332 #[builder(setter(into, strip_option), default = vec![])]
338 pub themes_folders: Vec<String>,
339
340 #[builder(setter(into, strip_option), default = vec![])]
348 pub fonts_folders: Vec<String>,
349
350 #[builder(setter(into), default = String::from("candy"))]
357 pub theme: String,
358
359 #[builder(setter(into))]
360 pub background: Background,
361
362 #[builder(setter(into), default = String::from("#495162"))]
363 pub line_number_color: String,
364
365 #[builder(setter(into, strip_option), default = None)]
366 pub title: Option<String>,
367}
368
369impl CodeSnap {
370 fn validate(&self) -> Result<(), String> {
371 if let Some(scale_factor) = self.scale_factor {
372 if scale_factor < 1 {
373 return Err("The scale factor must be greater than 1".to_string());
374 }
375 }
376
377 Ok(())
378 }
379
380 pub fn from_default_theme() -> Result<CodeSnap, serde_json::Error> {
381 Self::from_theme("bamboo")
382 }
383
384 pub fn from_theme(theme_name: &str) -> Result<CodeSnap, serde_json::Error> {
385 let theme = get_theme(theme_name);
386
387 Self::from_config(&theme)
388 }
389
390 pub fn from_config(config: &str) -> Result<CodeSnap, serde_json::Error> {
391 serde_json::from_str::<CodeSnap>(config)
392 }
393
394 pub fn map_code_config<F>(&mut self, f: F) -> anyhow::Result<&mut Self>
395 where
396 F: Fn(CodeConfig) -> anyhow::Result<CodeConfig>,
397 {
398 self.code_config = Some(f(self
399 .code_config
400 .clone()
401 .unwrap_or(CodeConfigBuilder::default().build()?))?);
402
403 Ok(self)
404 }
405
406 pub fn map_content<F>(&mut self, f: F) -> anyhow::Result<&mut Self>
407 where
408 F: Fn(Code) -> anyhow::Result<Content>,
409 {
410 let content = self.content.clone().unwrap_or(Content::Code(
411 CodeBuilder::default().content(String::from("")).build()?,
412 ));
413 let code_content = match content {
414 Content::Code(code_content) => code_content,
415 _ => return Ok(self),
416 };
417
418 self.content = Some(f(code_content)?);
419
420 Ok(self)
421 }
422
423 pub fn map_window<F>(&mut self, f: F) -> anyhow::Result<&mut Self>
424 where
425 F: Fn(Window) -> anyhow::Result<Window>,
426 {
427 self.window = Some(f(self
428 .window
429 .clone()
430 .unwrap_or(WindowBuilder::default().build()?))?);
431
432 Ok(self)
433 }
434
435 pub fn map_watermark<F>(&mut self, f: F) -> anyhow::Result<&mut Self>
436 where
437 F: Fn(Option<Watermark>) -> anyhow::Result<Option<Watermark>>,
438 {
439 self.watermark = Some(f(self.watermark.clone().unwrap_or(None))?);
440
441 Ok(self)
442 }
443}
444
445impl SnapshotConfig {
446 pub fn create_snapshot(&self) -> anyhow::Result<ImageSnapshot> {
448 ImageSnapshot::from_config(self.clone())
449 }
450
451 pub fn create_ascii_snapshot(&self) -> anyhow::Result<ASCIISnapshot> {
464 ASCIISnapshot::from_config(self.clone())
465 }
466}
467
468fn default_scale_factor() -> u8 {
469 3
470}