Skip to main content

codesnap/
config.rs

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    /// The `language` will be used to determine the syntax highlighting to use for generating
224    /// the snapshot.
225    #[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(""))]
261    // #[serde(default)]
262    // pub content: String,
263    #[builder(setter(into), default = String::from("CaskaydiaCove Nerd Font"))]
264    pub font_family: String,
265
266    /// Breadcrumbs is a useful and unique feature of CodeSnap, it can help users to understand the
267    /// code location in the project. If the `has_breadcrumbs` is true, CodeSnap will display the
268    /// `file_path` on top of the code.
269    ///
270    /// The code snapshot is different from normal screenshots, it should provide more information
271    /// about the code, such as the file path, the line number and highlight code line, these
272    /// information can help users to understand the code better.
273    #[builder(setter(into, strip_option), default = BreadcrumbsBuilder::default().build().unwrap())]
274    #[serde(default)]
275    pub breadcrumbs: Breadcrumbs,
276}
277
278/// Draw a watermark below the code, you can use this to add a logo or any other text
279/// The watermark is designed as a place for users to provide personalize label
280#[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    /// The code to be displayed in the snapshot
314    #[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    /// CodeSnap default generate triple size snapshot image,
327    /// you can use this config to change the scale factor.
328    #[builder(default = 3)]
329    #[serde(default = "default_scale_factor")]
330    pub scale_factor: u8,
331
332    /// CodeSnap use Syntect as the syntax highlighting engine, you can provide a custom theme
333    /// for the snapshot. If the `themes_folders` is provided, CodeSnap will load the theme from
334    /// the folder, otherwise, CodeSnap will load the default themes.
335    ///
336    /// Visit https://github.com/trishume/syntect for more detail
337    #[builder(setter(into, strip_option), default = vec![])]
338    pub themes_folders: Vec<String>,
339
340    /// Load fonts from the fonts_folders to render the code, CodeSnap use fonts which you have
341    /// installed on your system by default, but you can still provide `fonts_folders` to tell
342    /// CodeSnap to load extra fonts from the folder.
343    ///
344    /// This config is useful when you want to develop a tool based on CodeSnap, you can package
345    /// some fonts with your tool and publish, so that users can use these fonts without installing
346    /// them manually on their system.
347    #[builder(setter(into, strip_option), default = vec![])]
348    pub fonts_folders: Vec<String>,
349
350    /// CodeSnap use Syntect as the syntax highlighting engine, you can provide a custom theme
351    /// for code highlighting and background.
352    /// The theme is load from the `themes_folders`(if not provided, CodeSnap load the default
353    /// themes), you can use the theme name to specify the theme you want to use.
354    ///
355    /// See `themes_folders` config for more detail.
356    #[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    /// Create a beautiful code snapshot from the config
447    pub fn create_snapshot(&self) -> anyhow::Result<ImageSnapshot> {
448        ImageSnapshot::from_config(self.clone())
449    }
450
451    /// Create a ASCII "snapshot" from the config, the ASCII "snapshot" is a text representation of
452    /// the code, it's useful when you want to display the code in the terminal or other text-based
453    /// environment, and because of it's text-based, you can easily copy and paste it to anywhere.
454    ///
455    /// Through the ASCII "snapshot" is text-based, but it still has some basic styles, and it's
456    /// will take some important information of code, such as the `line number` and `file path`,
457    /// these information can help users to understand the code better.
458    ///
459    /// And If you want to highlighting the ASCII "snapshot", you can try put it into a markdown
460    /// code block, most markdown renderers will highlight the code block for you.
461    ///
462    /// The ASCII "snapshot" is really cool, hope you like it!
463    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}