use std::ffi::c_void;
use anyhow::{Result, bail};
use image::RgbaImage;
#[link(name = "CoreGraphics", kind = "framework")]
unsafe extern "C" {
fn CGPreflightScreenCaptureAccess() -> bool;
fn CGRequestScreenCaptureAccess() -> bool;
fn CGDisplayCreateImage(display: u32) -> *mut c_void;
fn CGImageGetWidth(image: *mut c_void) -> usize;
fn CGImageGetHeight(image: *mut c_void) -> usize;
fn CGImageGetBytesPerRow(image: *mut c_void) -> usize;
fn CGImageGetBitsPerPixel(image: *mut c_void) -> usize;
fn CGImageGetDataProvider(image: *mut c_void) -> *mut c_void;
fn CGDataProviderCopyData(provider: *mut c_void) -> *const c_void;
fn CGImageRelease(image: *mut c_void);
}
#[link(name = "CoreFoundation", kind = "framework")]
unsafe extern "C" {
fn CFDataGetBytePtr(data: *const c_void) -> *const u8;
fn CFDataGetLength(data: *const c_void) -> isize;
fn CFRelease(cf: *const c_void);
}
pub fn capture_display(display: u32) -> Result<RgbaImage> {
unsafe {
let image = CGDisplayCreateImage(display);
if image.is_null() {
bail!(
"CGDisplayCreateImage returned nothing for display {display} — the display may have been disconnected mid-capture"
);
}
let result = copy_pixels(image);
CGImageRelease(image);
result
}
}
const SUPPORTED_BITS_PER_PIXEL: usize = 32;
unsafe fn copy_pixels(image: *mut c_void) -> Result<RgbaImage> {
unsafe {
let width = CGImageGetWidth(image);
let height = CGImageGetHeight(image);
let bytes_per_row = CGImageGetBytesPerRow(image);
let bits_per_pixel = CGImageGetBitsPerPixel(image);
if bits_per_pixel != SUPPORTED_BITS_PER_PIXEL {
bail!(
"the display returned {bits_per_pixel}-bit pixels; pixelcoords reads \
{SUPPORTED_BITS_PER_PIXEL}-bit BGRA. Reading them anyway would produce a \
wrong image rather than an error, so it stops here — please report this \
with your display setup at \
https://github.com/nolindnaidoo/pixelcoords/issues"
);
}
if bytes_per_row == 0 || bytes_per_row < width * 4 {
bail!(
"the display reported a {bytes_per_row}-byte stride for {width}-pixel rows, \
which cannot hold them"
);
}
let provider = CGImageGetDataProvider(image);
if provider.is_null() {
bail!("captured image had no data provider");
}
let data = CGDataProviderCopyData(provider);
if data.is_null() {
bail!("could not copy the captured image's pixels");
}
let bytes = CFDataGetBytePtr(data);
let length = CFDataGetLength(data);
if bytes.is_null() || length < 0 {
CFRelease(data);
bail!("captured image had an unreadable pixel buffer");
}
let raw = std::slice::from_raw_parts(bytes, length as usize);
let mut buffer = Vec::with_capacity(width * height * 4);
for row in raw.chunks_exact(bytes_per_row).take(height) {
buffer.extend_from_slice(&row[..width * 4]);
}
for pixel in buffer.chunks_exact_mut(4) {
pixel.swap(0, 2);
}
CFRelease(data);
RgbaImage::from_raw(width as u32, height as u32, buffer).ok_or_else(|| {
anyhow::anyhow!("captured {width}x{height} image did not fit its buffer")
})
}
}
pub fn has_screen_capture_access() -> bool {
unsafe { CGPreflightScreenCaptureAccess() }
}
pub fn request_screen_capture_access() -> bool {
unsafe { CGRequestScreenCaptureAccess() }
}
pub const GRANT_INSTRUCTIONS: &str = "\
Screen Recording permission is NOT granted.
pixelcoords can only capture your wallpaper until it is.
To grant it:
1. Open System Settings -> Privacy & Security -> Screen & System Audio Recording
2. Enable the terminal app you run pixelcoords from (Terminal, iTerm2, Ghostty, VS Code, ...)
3. Quit and reopen that terminal app, then run `pixelcoords doctor` again
If the app is missing from the list, run `pixelcoords` once to trigger the
permission prompt, or add the terminal manually with the '+' button.";