mod code; mod code_config; mod config; mod egg; mod highlight; mod logger; mod range; mod watermark; mod window; use std::fs::{read_to_string, File}; use std::io::{self, BufWriter, Write}; use anyhow::bail; use clap::value_parser; use clap::Parser; use code::create_content; use code_config::create_code_config; use codesnap::config::CodeSnap; use codesnap::config::SnapshotConfig; use codesnap::themes::parse_code_theme; use codesnap::utils::path::parse_file_name; use config::CodeSnapCLIConfig; use egg::say; use watermark::create_watermark; use window::create_window; // use std::thread; use std::time::Duration; use indicatif::{ProgressBar, ProgressStyle}; pub const STDIN_CODE_DEFAULT_CHAR: &str = "-"; /// CodeSnap is a CLI tool to generate beautiful snapshots of your code from terminal. #[derive(Parser)] #[command(bin_name = "codesnap")] #[command(version, about, long_about = None)] #[command(propagate_version = true)] struct CLI { /// Path to the file to snapshot #[arg(short = 'f', long)] from_file: Option, /// Code snippet for snapshot #[arg(short = 'c', long, default_missing_value = STDIN_CODE_DEFAULT_CHAR, require_equals=false, num_args=0..=1, value_parser=value_parser!(String), value_name="Code")] from_code: Option, #[arg(short = 'i', long, default_missing_value = STDIN_CODE_DEFAULT_CHAR, require_equals=false, num_args=0..=1, value_parser=value_parser!(String), value_name="Image")] from_image: Option, #[arg(long)] from_clipboard: bool, /// Output path for the snapshot. /// Available value: /// /// - clipboard: Copy the snapshot to clipboard /// - file path: Save the snapshot to the file path /// /// Currently CodeSnap supports SVG format and PNG format /// If output is directory, CodeSnap will generate a temporary file name to save the snapshot /// to the directory. #[arg(short, long)] output: String, #[arg(long, default_value = "false")] silent: bool, /// Executing a command and taking the output as the code snippet. #[arg(long, short, num_args=1..)] execute: Vec, /// Skip run the command to get output, just take the command as the input #[arg(long)] skip: bool, /// You can set the range of the code snippet to display /// for example, display the 3rd to 5th: /// 3:5 /// The syntax is similar to the range in Python or Golang, so basically you can use 3: to /// represent the 3rd to the end, or use :5 to represent the start to the 5th. /// This option is useful when you use the `from_file` option as the input, and you just want /// to display part of the code snippet. #[arg(long)] range: Option, /// Font family for the code snippet #[arg(long)] code_font_family: Option, /// Code theme for the code snippet #[arg(long)] code_theme: Option, /// Breadcrumbs is a useful and unique feature in CodeSnap, it shows the path of the file /// so that users can know where the code snippet comes from. #[arg(long, num_args = 0..=1, default_missing_value = "true")] has_breadcrumbs: Option, #[arg(long, default_value = "false")] has_line_number: bool, /// Breadcrumbs separator is the character to separate the path in breadcrumbs /// Default is `/` #[arg(long)] breadcrumbs_separator: Option, /// Breadcrumbs font family #[arg(long)] breadcrumbs_font_family: Option, /// Breadcrumbs font color #[arg(long)] breadcrumbs_color: Option, /// Set start line number to display line numbers #[arg(long)] start_line_number: Option, /// Line number font color #[arg(long)] line_number_color: Option, /// Delete lines will be marked with a red line #[arg(long, short, num_args=1..)] delete_line: Vec, /// Delete line color #[arg(long, default_value = "#ff6b6b30")] delete_line_color: String, /// New lines will be marked with a green line #[arg(long, short, num_args=1..)] add_line: Vec, /// New line color #[arg(long, default_value = "#2ecc7130")] add_line_color: String, /// Convenient version of `--raw-highlight-lines` option, you can set the highlight range /// with a simple syntax, for example, highlight the 3rd to 5th lines: /// 3:5 #[arg(long)] highlight_range: Option, #[arg(long)] relative_highlight_range: bool, /// Highlight color for the highlighted code lines #[arg(long, default_value = "#ffffff10")] highlight_range_color: String, /// CodeSnap allows users to highlight multiple lines with custom highlight color in the code snippet /// The content of highlight_lines is JSON string, for example highlight the first line and the /// 3rd to 5th lines: /// "[ /// [1, "#ff0000"], /// [3, 5, "#00ff00"] /// ]" /// #[arg(long)] raw_highlight_lines: Option, /// Set the language of the code snippet, If you using the `file` option, CodeSnap will /// automatically detect the language from the file extension. #[arg(long, short)] language: Option, /// Used to detect the language of the code snippet, if you want to set language manually, /// use `--language` or `-l` option. #[arg(long)] file_path: Option, /// Disables the rendering of the watermark #[arg(long, default_value_t = false)] disable_watermark: bool, /// Set watermark for the code snippet #[arg(long, short)] watermark: Option, /// Watermark font family #[arg(long)] watermark_font_family: Option, /// Watermark font color #[arg(long)] watermark_color: Option, /// Set window shadow radius #[arg(long)] shadow_radius: Option, #[arg(long)] shadow_color: Option, /// Display MacOS style window bar #[arg(long)] mac_window_bar: Option, /// Display window border #[arg(long, num_args = 0..=1, default_missing_value = "true")] has_border: Option, /// Window border color #[arg(long)] border_color: Option, /// Set horizontal margin of window #[arg(long)] margin_x: Option, /// Set vertical margin of window #[arg(long)] margin_y: Option, /// Set title of the window #[arg(long)] title: Option, /// CodeSnap supports scaling the snapshot, the default scale factor is 3 for better quality #[arg(long)] scale_factor: Option, /// Title font family #[arg(long)] title_font_family: Option, /// Title font color #[arg(long)] title_color: Option, /// Set background color of the snapshot #[arg(long)] background: Option, #[arg(long, value_parser=["ascii", "image"], default_value="image")] r#type: String, #[arg(long)] config: Option, } fn output_snapshot(cli: &CLI, snapshot: &SnapshotConfig) -> anyhow::Result { if cli.output == "raw" { let mut stdout = io::stdout().lock(); snapshot.write_png(&mut stdout)?; stdout.flush()?; return Ok("Output to stdout".to_string()); } // Save snapshot to clipboard if cli.output == "clipboard" { match cli.r#type.as_str() { "ascii" => { snapshot.create_ascii_snapshot()?.raw_data()?.copy()?; } "image" => { snapshot.create_snapshot()?.raw_data()?.copy()?; } _ => { bail!("Invalid snapshot type"); } } return Ok("Snapshot copied to clipboard".to_string()); } // Save snapshot to file match cli.output.as_str() { output if output.ends_with(".png") => { let path = parse_file_name(&cli.output)?; let mut output = BufWriter::new(File::create(path)?); snapshot.write_png(&mut output)?; output.flush()?; } output if output.ends_with(".svg") => { snapshot.create_snapshot()?.svg_data()?.save(&cli.output)?; } output if output.ends_with(".html") => { snapshot.create_snapshot()?.html_data()?.save(&cli.output)?; } _ => { bail!("Unsupported output format"); } }; Ok(format!("Snapshot saved to {} successful!", cli.output)) } async fn generate_snapshot_with_config(cli: &CLI, codesnap: CodeSnap) -> anyhow::Result<()> { let snapshot = create_snapshot_config(cli, codesnap).await?; let snapshot_type = cli.r#type.clone(); if snapshot_type == "ascii" && cli.output != "clipboard" { logger::warn("ASCII snapshot only supports copying to clipboard"); return Ok(()); } if !cli.silent { let message = with_spinner(|| output_snapshot(cli, &snapshot))?; logger::success(&message) } else { output_snapshot(cli, &snapshot)?; } Ok(()) } fn build_snapshot_config(cli: &CLI, mut codesnap: CodeSnap) -> anyhow::Result { codesnap .map_code_config(|code_config| create_code_config(cli, code_config))? .map_content(|default_content| create_content(cli, default_content))? .map_watermark(|watermark| create_watermark(cli, watermark))? .map_window(|window| create_window(cli, window))?; if let Some(scale_factor) = cli.scale_factor { codesnap.scale_factor(scale_factor); } if let Some(ref line_number_color) = cli.line_number_color { codesnap.line_number_color(line_number_color.clone()); } if let Some(ref title) = cli.title { codesnap.title(title.clone()); } Ok(codesnap.build()?) } async fn create_snapshot_config(cli: &CLI, codesnap: CodeSnap) -> anyhow::Result { let mut codesnap = build_snapshot_config(cli, codesnap)?; codesnap.theme = parse_code_theme( cli.code_theme .clone() .unwrap_or(codesnap.theme.clone()) .as_ref(), ) .await?; Ok(codesnap) } fn with_spinner(cb: impl Fn() -> T) -> T { let pb = ProgressBar::new_spinner(); pb.enable_steady_tick(Duration::from_millis(120)); pb.set_style( ProgressStyle::with_template("{spinner:.green} {msg}") .unwrap() .tick_strings(&[ "⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣯", "⣷", "⠁", "⠂", "⠄", "⡀", "⢀", "⠠", "⠠", "⠐", "⠈", ]), ); pb.set_message("Generating..."); let result = cb(); pb.finish_and_clear(); result } async fn generate_snapshot() -> anyhow::Result<()> { let cli = CLI::parse(); // Create CodeSnap config from config, if the user does not have a config file, we will create // a default CodeSnap config to $HOME/.codesnap/config.json for the user. let codesnap_cli_config = if let Some(ref config) = cli.config { let content = read_to_string(config)?; CodeSnapCLIConfig::from(&content)? } else { CodeSnapCLIConfig::from(&config::get_config_content()?)? }; generate_snapshot_with_config(&cli, codesnap_cli_config.snapshot_config).await?; if codesnap_cli_config.print_eggs { say(); } Ok(()) } #[tokio::main] async fn main() { if let Err(err) = generate_snapshot().await { logger::error(&err.to_string()); return; }; } #[cfg(test)] mod tests;