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