praefectus 0.8.0

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};

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()))
}