halley-capit 0.1.0

Screen capture and screencopy library for the Halley Wayland compositor.
Documentation
use std::fs;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};

use image::{DynamicImage, GenericImageView};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CaptureCrop {
    pub x: i32,
    pub y: i32,
    pub w: i32,
    pub h: i32,
}

pub fn crop_image(img: DynamicImage, crop: CaptureCrop) -> Result<DynamicImage, String> {
    let (iw, ih) = img.dimensions();
    let x0 = crop.x.max(0) as u32;
    let y0 = crop.y.max(0) as u32;
    let x1 = (crop.x.max(0) as u32)
        .saturating_add(crop.w.max(0) as u32)
        .min(iw);
    let y1 = (crop.y.max(0) as u32)
        .saturating_add(crop.h.max(0) as u32)
        .min(ih);
    let cw = x1.saturating_sub(x0);
    let ch = y1.saturating_sub(y0);
    if cw == 0 || ch == 0 {
        return Err(format!(
            "crop rect empty after clamping: ({},{}) {}x{} within {}x{}",
            crop.x, crop.y, crop.w, crop.h, iw, ih
        ));
    }
    Ok(img.crop_imm(x0, y0, cw, ch))
}

pub fn save_cropped_png(src_path: &Path, out_path: &Path, crop: CaptureCrop) -> Result<(), String> {
    let img = image::open(src_path).map_err(|e| format!("open screenshot: {e}"))?;
    let cropped = crop_image(img, crop)?;
    ensure_parent_dir(out_path)?;
    cropped
        .save(out_path)
        .map_err(|e| format!("save cropped screenshot: {e}"))
}

pub fn default_output_path(stem: &str) -> PathBuf {
    default_output_path_in(
        std::env::var_os("HOME")
            .map(PathBuf::from)
            .unwrap_or_else(|| PathBuf::from("."))
            .join("Pictures")
            .join("Screenshots"),
        stem,
    )
}

pub fn default_output_path_in(dir: PathBuf, stem: &str) -> PathBuf {
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_nanos();
    dir.join(format!("{stem}-{nanos}.png"))
}

pub fn temp_output_path(final_out_path: &Path) -> PathBuf {
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_nanos();
    let mut p = final_out_path.to_path_buf();
    let stem = final_out_path
        .file_stem()
        .and_then(|s| s.to_str())
        .unwrap_or("capit");
    p.set_file_name(format!("{stem}.halley_capit_tmp_{nanos}.png"));
    p
}

pub(crate) fn ensure_parent_dir(path: &Path) -> Result<(), String> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).map_err(|e| format!("create dir {parent:?}: {e}"))?;
    }
    Ok(())
}