cryosnap-core 0.2.11

Core rendering library for cryosnap.
Documentation
use serde::{Deserialize, Serialize};

use crate::{
    DEFAULT_PNG_OPT_LEVEL, DEFAULT_PNG_QUANTIZE_DITHER, DEFAULT_PNG_QUANTIZE_QUALITY,
    DEFAULT_PNG_QUANTIZE_SPEED, DEFAULT_RASTER_MAX_PIXELS, DEFAULT_RASTER_SCALE,
    DEFAULT_TITLE_MAX_WIDTH, DEFAULT_TITLE_OPACITY, DEFAULT_TITLE_SIZE,
};

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct Config {
    pub theme: String,
    pub background: String,
    #[serde(deserialize_with = "deserialize_box")]
    pub padding: Vec<f32>,
    #[serde(deserialize_with = "deserialize_box")]
    pub margin: Vec<f32>,
    pub width: f32,
    pub height: f32,
    #[serde(rename = "window")]
    pub window_controls: bool,
    #[serde(rename = "show_line_numbers")]
    pub show_line_numbers: bool,
    pub language: Option<String>,
    pub execute_timeout_ms: u64,
    pub wrap: usize,
    #[serde(deserialize_with = "deserialize_lines")]
    pub lines: Vec<i32>,
    pub border: Border,
    pub shadow: Shadow,
    pub font: Font,
    #[serde(rename = "line_height")]
    pub line_height: f32,
    pub raster: RasterOptions,
    pub png: PngOptions,
    pub title: TitleOptions,
}

impl Default for Config {
    fn default() -> Self {
        Self {
            theme: "charm".to_string(),
            background: "#171717".to_string(),
            padding: vec![20.0, 40.0, 20.0, 20.0],
            margin: vec![0.0],
            width: 0.0,
            height: 0.0,
            window_controls: false,
            show_line_numbers: false,
            language: None,
            execute_timeout_ms: 10_000,
            wrap: 0,
            lines: vec![0, -1],
            border: Border::default(),
            shadow: Shadow::default(),
            font: Font::default(),
            line_height: 1.2,
            raster: RasterOptions::default(),
            png: PngOptions::default(),
            title: TitleOptions::default(),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct Border {
    pub radius: f32,
    pub width: f32,
    pub color: String,
}

impl Default for Border {
    fn default() -> Self {
        Self {
            radius: 0.0,
            width: 0.0,
            color: "#515151".to_string(),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct Shadow {
    pub blur: f32,
    pub x: f32,
    pub y: f32,
}

impl Default for Shadow {
    fn default() -> Self {
        Self {
            blur: 0.0,
            x: 0.0,
            y: 0.0,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct Font {
    pub family: String,
    pub file: Option<String>,
    pub size: f32,
    pub ligatures: bool,
    pub fallbacks: Vec<String>,
    #[serde(rename = "system_fallback")]
    pub system_fallback: FontSystemFallback,
    #[serde(rename = "auto_download")]
    pub auto_download: bool,
    #[serde(rename = "force_update")]
    pub force_update: bool,
    #[serde(rename = "cjk_region")]
    pub cjk_region: CjkRegion,
    #[serde(rename = "dirs")]
    pub dirs: Vec<String>,
}

impl Default for Font {
    fn default() -> Self {
        Self {
            family: "monospace".to_string(),
            file: None,
            size: 14.0,
            ligatures: true,
            fallbacks: Vec::new(),
            system_fallback: FontSystemFallback::default(),
            auto_download: true,
            force_update: false,
            cjk_region: CjkRegion::default(),
            dirs: Vec::new(),
        }
    }
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum FontSystemFallback {
    #[default]
    Auto,
    Always,
    Never,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum CjkRegion {
    #[default]
    Auto,
    Sc,
    Tc,
    Hk,
    Jp,
    Kr,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct RasterOptions {
    pub scale: f32,
    pub max_pixels: u64,
    pub backend: RasterBackend,
}

impl Default for RasterOptions {
    fn default() -> Self {
        Self {
            scale: DEFAULT_RASTER_SCALE,
            max_pixels: DEFAULT_RASTER_MAX_PIXELS,
            backend: RasterBackend::Auto,
        }
    }
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum RasterBackend {
    #[default]
    Auto,
    Resvg,
    Rsvg,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum TitleAlign {
    Left,
    #[default]
    Center,
    Right,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum TitlePathStyle {
    #[default]
    Absolute,
    Relative,
    Basename,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct TitleOptions {
    pub enabled: bool,
    pub text: Option<String>,
    pub path_style: TitlePathStyle,
    pub tmux_format: String,
    pub align: TitleAlign,
    pub size: f32,
    pub color: String,
    pub opacity: f32,
    pub max_width: usize,
    pub ellipsis: String,
}

impl Default for TitleOptions {
    fn default() -> Self {
        Self {
            enabled: true,
            text: None,
            path_style: TitlePathStyle::Absolute,
            tmux_format: "#{session_name}:#{window_index}.#{pane_index} #{pane_title}".to_string(),
            align: TitleAlign::Center,
            size: DEFAULT_TITLE_SIZE,
            color: "#C5C8C6".to_string(),
            opacity: DEFAULT_TITLE_OPACITY,
            max_width: DEFAULT_TITLE_MAX_WIDTH,
            ellipsis: "…".to_string(),
        }
    }
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum PngStrip {
    None,
    #[default]
    Safe,
    All,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PngQuantPreset {
    Fast,
    Balanced,
    Best,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct PngOptions {
    pub optimize: bool,
    pub level: u8,
    pub strip: PngStrip,
    pub quantize: bool,
    pub quantize_preset: Option<PngQuantPreset>,
    pub quantize_quality: u8,
    pub quantize_speed: u8,
    pub quantize_dither: f32,
}

impl Default for PngOptions {
    fn default() -> Self {
        Self {
            optimize: true,
            level: DEFAULT_PNG_OPT_LEVEL,
            strip: PngStrip::Safe,
            quantize: false,
            quantize_preset: None,
            quantize_quality: DEFAULT_PNG_QUANTIZE_QUALITY,
            quantize_speed: DEFAULT_PNG_QUANTIZE_SPEED,
            quantize_dither: DEFAULT_PNG_QUANTIZE_DITHER,
        }
    }
}

fn deserialize_box<'de, D>(deserializer: D) -> std::result::Result<Vec<f32>, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let value = serde_json::Value::deserialize(deserializer)?;
    parse_box_value(&value).map_err(serde::de::Error::custom)
}

fn parse_box_value(value: &serde_json::Value) -> std::result::Result<Vec<f32>, String> {
    match value {
        serde_json::Value::Number(n) => n
            .as_f64()
            .map(|v| vec![v as f32])
            .ok_or_else(|| "invalid number".to_string()),
        serde_json::Value::String(s) => parse_box_string(s),
        serde_json::Value::Array(arr) => {
            let mut out = Vec::new();
            for item in arr {
                match item {
                    serde_json::Value::Number(n) => {
                        out.push(n.as_f64().ok_or_else(|| "invalid number".to_string())? as f32);
                    }
                    serde_json::Value::String(s) => {
                        let parsed = parse_box_string(s)?;
                        out.extend(parsed);
                    }
                    _ => return Err("invalid array value".to_string()),
                }
            }
            if matches!(out.len(), 1 | 2 | 4) {
                Ok(out)
            } else {
                Err(format!("expected 1, 2, or 4 values, got {}", out.len()))
            }
        }
        serde_json::Value::Null => Ok(vec![0.0]),
        _ => Err("invalid box value".to_string()),
    }
}

fn parse_box_string(input: &str) -> std::result::Result<Vec<f32>, String> {
    let parts: Vec<&str> = input.split([',', ' ']).filter(|s| !s.is_empty()).collect();
    if parts.is_empty() {
        return Ok(vec![0.0]);
    }
    let mut out = Vec::new();
    for part in parts {
        let value = part
            .parse::<f32>()
            .map_err(|_| format!("invalid number {}", part))?;
        out.push(value);
    }
    if matches!(out.len(), 1 | 2 | 4) {
        Ok(out)
    } else {
        Err(format!("expected 1, 2, or 4 values, got {}", out.len()))
    }
}

fn deserialize_lines<'de, D>(deserializer: D) -> std::result::Result<Vec<i32>, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let value = serde_json::Value::deserialize(deserializer)?;
    parse_lines_value(&value).map_err(serde::de::Error::custom)
}

fn parse_lines_value(value: &serde_json::Value) -> std::result::Result<Vec<i32>, String> {
    match value {
        serde_json::Value::Number(n) => n
            .as_i64()
            .map(|v| vec![v as i32])
            .ok_or_else(|| "invalid number".to_string()),
        serde_json::Value::String(s) => parse_lines_string(s),
        serde_json::Value::Array(arr) => {
            let mut out = Vec::new();
            for item in arr {
                match item {
                    serde_json::Value::Number(n) => {
                        out.push(n.as_i64().ok_or_else(|| "invalid number".to_string())? as i32);
                    }
                    serde_json::Value::String(s) => {
                        let parsed = parse_lines_string(s)?;
                        out.extend(parsed);
                    }
                    _ => return Err("invalid array value".to_string()),
                }
            }
            if matches!(out.len(), 1 | 2) {
                Ok(out)
            } else {
                Err(format!("expected 1 or 2 values, got {}", out.len()))
            }
        }
        serde_json::Value::Null => Ok(vec![]),
        _ => Err("invalid lines value".to_string()),
    }
}

fn parse_lines_string(input: &str) -> std::result::Result<Vec<i32>, String> {
    let parts: Vec<&str> = input.split([',', ' ']).filter(|s| !s.is_empty()).collect();
    if parts.is_empty() {
        return Ok(vec![]);
    }
    let mut out = Vec::new();
    for part in parts {
        let value = part
            .parse::<i32>()
            .map_err(|_| format!("invalid number {}", part))?;
        out.push(value);
    }
    if matches!(out.len(), 1 | 2) {
        Ok(out)
    } else {
        Err(format!("expected 1 or 2 values, got {}", out.len()))
    }
}