praefectus 0.8.2

Policy-neutral, durable desktop action execution protocol
Documentation
use foreign_types::ForeignTypeRef;
use screencapturekit::CGImage;
use screencapturekit::prelude::{
    SCContentFilter, SCDisplay, SCShareableContent, SCStreamConfiguration,
};
use screencapturekit::screenshot_manager::SCScreenshotManager;
use sha2::{Digest, Sha256};
use std::path::Path;

pub(crate) struct CaptureError;

fn configuration(width: u32, height: u32) -> SCStreamConfiguration {
    SCStreamConfiguration::new()
        .with_width(width.max(1))
        .with_height(height.max(1))
        .with_shows_cursor(false)
}

fn image_digest(image: &CGImage, hasher: &mut Sha256) {
    hasher.update(image.width().to_be_bytes());
    hasher.update(image.height().to_be_bytes());
    hasher.update(image.bytes_per_row().to_be_bytes());
    let pointer = image.as_ptr();
    if pointer.is_null() {
        return;
    }
    let borrowed = unsafe { core_graphics::image::CGImageRef::from_ptr(pointer.cast()) };
    hasher.update(borrowed.data().as_ref());
}

fn displays() -> Result<Vec<SCDisplay>, CaptureError> {
    Ok(SCShareableContent::get()
        .map_err(|_| CaptureError)?
        .displays())
}

fn capture(filter: &SCContentFilter, width: u32, height: u32) -> Result<CGImage, CaptureError> {
    SCScreenshotManager::capture_image(filter, &configuration(width, height))
        .map_err(|_| CaptureError)
}

const CAPTURE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);

pub(crate) fn screen_content_hash() -> Result<String, CaptureError> {
    let (sender, receiver) = std::sync::mpsc::channel();
    std::thread::Builder::new()
        .spawn(move || {
            let _ = sender.send(blocking_screen_content_hash());
        })
        .map_err(|_| CaptureError)?;
    receiver
        .recv_timeout(CAPTURE_TIMEOUT)
        .map_err(|_| CaptureError)?
}

fn blocking_screen_content_hash() -> Result<String, CaptureError> {
    let displays = displays()?;
    if displays.is_empty() {
        return Err(CaptureError);
    }
    let mut hasher = Sha256::new();
    for display in displays {
        let frame = display.frame();
        let filter = SCContentFilter::create()
            .with_display(&display)
            .with_excluding_windows(&[])
            .build();
        let image = capture(&filter, display.width(), display.height())?;
        hasher.update(display.display_id().to_be_bytes());
        hasher.update(frame.size.width.to_bits().to_be_bytes());
        hasher.update(frame.size.height.to_bits().to_be_bytes());
        image_digest(&image, &mut hasher);
    }
    Ok(hex::encode(hasher.finalize()))
}

pub(crate) fn capture_screen(path: &Path) -> Result<(), CaptureError> {
    let path = path.to_path_buf();
    let (sender, receiver) = std::sync::mpsc::channel();
    std::thread::Builder::new()
        .spawn(move || {
            let _ = sender.send(blocking_capture_screen(&path));
        })
        .map_err(|_| CaptureError)?;
    receiver
        .recv_timeout(CAPTURE_TIMEOUT)
        .map_err(|_| CaptureError)?
}

fn blocking_capture_screen(path: &Path) -> Result<(), CaptureError> {
    let display = displays()?.into_iter().next().ok_or(CaptureError)?;
    let filter = SCContentFilter::create()
        .with_display(&display)
        .with_excluding_windows(&[])
        .build();
    let image = capture(&filter, display.width(), display.height())?;
    image
        .save_png(path.to_str().ok_or(CaptureError)?)
        .map_err(|_| CaptureError)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn configuration_clamps_zero_dimensions_and_hides_the_cursor() {
        let config = configuration(0, 0);
        assert_eq!(config.width(), 1);
        assert_eq!(config.height(), 1);
        assert!(!config.shows_cursor());
    }

    #[test]
    fn configuration_preserves_nonzero_dimensions() {
        let config = configuration(1920, 1080);
        assert_eq!(config.width(), 1920);
        assert_eq!(config.height(), 1080);
        assert!(!config.shows_cursor());
    }
}