use chrono::Utc;
use core_foundation::base::{CFType, TCFType};
use core_foundation::dictionary::CFDictionary;
use core_foundation::number::CFNumber;
use core_foundation::string::CFString;
use core_graphics::display::CGDisplay;
use core_graphics::geometry::{CGPoint, CGRect, CGSize};
use core_graphics::image::CGImageRef;
use core_graphics::window::{
copy_window_info, create_image, kCGWindowImageBoundsIgnoreFraming,
kCGWindowListOptionIncludingWindow, kCGWindowListOptionOnScreenOnly, kCGWindowNumber,
};
use crate::errors::{CarDesktopError, Result};
use crate::models::{DisplayId, Frame, WindowHandle};
pub fn capture_display_impl(display: DisplayId) -> Result<Frame> {
if let Some(frame) = sck::try_capture_display(display)? {
return Ok(frame);
}
capture_display_via_cg(display)
}
fn capture_display_via_cg(display: DisplayId) -> Result<Frame> {
let cg_display = if display == DisplayId::PRIMARY {
CGDisplay::main()
} else {
CGDisplay::new(display.0 as u32)
};
let image = cg_display.image().ok_or_else(|| CarDesktopError::OsApi {
detail: format!(
"CGDisplayCreateImage returned null for display {}",
display.0
),
source: None,
})?;
frame_from_cgimage(&image)
}
pub fn capture_window_impl(handle: WindowHandle) -> Result<Frame> {
let frame = lookup_window_frame(handle)?;
let rect = CGRect::new(
&CGPoint::new(frame.x, frame.y),
&CGSize::new(frame.width.max(1.0), frame.height.max(1.0)),
);
let image = create_image(
rect,
kCGWindowListOptionIncludingWindow,
handle.window_id as u32,
kCGWindowImageBoundsIgnoreFraming,
)
.ok_or_else(|| CarDesktopError::OsApi {
detail: format!(
"CGWindowListCreateImage returned null for window {}:{}",
handle.pid, handle.window_id
),
source: None,
})?;
frame_from_cgimage(&image)
}
fn lookup_window_frame(handle: WindowHandle) -> Result<WindowFrame> {
let list = copy_window_info(
kCGWindowListOptionOnScreenOnly | kCGWindowListOptionIncludingWindow,
handle.window_id as u32,
)
.ok_or_else(|| CarDesktopError::OsApi {
detail: "CGWindowListCopyWindowInfo returned null in lookup_window_frame".into(),
source: None,
})?;
let count = list.len();
for i in 0..count {
let Some(dict_ref) = list.get(i) else {
continue;
};
let dict_type_ref = *dict_ref as core_foundation::base::CFTypeRef;
let dict: CFDictionary<CFString, CFType> =
unsafe { CFDictionary::wrap_under_get_rule(dict_type_ref as _) };
let id = {
let key = unsafe { CFString::wrap_under_get_rule(kCGWindowNumber) };
let Some(v) = dict.find(&key) else { continue };
let Some(n): Option<CFNumber> = v.downcast::<CFNumber>() else {
continue;
};
n.to_i64().unwrap_or(-1)
};
if id as u64 != handle.window_id {
continue;
}
return extract_bounds(&dict).ok_or_else(|| CarDesktopError::WindowNotFound {
detail: format!(
"window {}:{} missing kCGWindowBounds",
handle.pid, handle.window_id
),
});
}
Err(CarDesktopError::WindowNotFound {
detail: format!(
"window {}:{} no longer on-screen",
handle.pid, handle.window_id
),
})
}
#[derive(Debug, Clone, Copy)]
struct WindowFrame {
x: f64,
y: f64,
width: f64,
height: f64,
}
fn extract_bounds(dict: &CFDictionary<CFString, CFType>) -> Option<WindowFrame> {
let key = unsafe { CFString::wrap_under_get_rule(core_graphics::window::kCGWindowBounds) };
let value = dict.find(&key)?;
let untyped: CFDictionary = value.downcast::<CFDictionary>()?;
let bounds: CFDictionary<CFString, CFType> =
unsafe { CFDictionary::wrap_under_get_rule(untyped.as_concrete_TypeRef()) };
let get = |k: &str| -> Option<f64> {
let key = CFString::new(k);
let v = bounds.find(&key)?;
let n: CFNumber = v.downcast::<CFNumber>()?;
n.to_f64().or_else(|| n.to_i64().map(|i| i as f64))
};
Some(WindowFrame {
x: get("X")?,
y: get("Y")?,
width: get("Width")?,
height: get("Height")?,
})
}
fn frame_from_cgimage(image: &CGImageRef) -> Result<Frame> {
let width = image.width() as u32;
let height = image.height() as u32;
let bits_per_pixel = image.bits_per_pixel();
if bits_per_pixel != 32 {
return Err(CarDesktopError::OsApi {
detail: format!("unexpected CGImage bits_per_pixel = {bits_per_pixel} (expected 32)"),
source: None,
});
}
let bytes_per_row = image.bytes_per_row() as usize;
let native = image.data().to_vec();
frame_from_cgimage_bytes(&native, width, height, bytes_per_row)
}
fn frame_from_cgimage_bytes(
native: &[u8],
width: u32,
height: u32,
bytes_per_row: usize,
) -> Result<Frame> {
let stride = width as usize * 4;
let expected_len = bytes_per_row * height as usize;
if native.len() < expected_len {
return Err(CarDesktopError::OsApi {
detail: format!(
"framebuffer length {} less than bytes_per_row*height {}",
native.len(),
expected_len,
),
source: None,
});
}
let mut rgba = Vec::with_capacity(stride * height as usize);
for row in 0..height as usize {
let src_start = row * bytes_per_row;
let src_end = src_start + stride;
let row_start = rgba.len();
rgba.extend_from_slice(&native[src_start..src_end]);
for px in rgba[row_start..].chunks_exact_mut(4) {
px.swap(0, 2);
}
}
let frame = Frame {
width,
height,
scale_factor: 1.0,
rgba,
captured_at: Utc::now(),
};
frame.validate().map_err(|detail| CarDesktopError::OsApi {
detail,
source: None,
})?;
Ok(frame)
}
#[cfg(test)]
mod tests {
#[test]
fn scale_factor_is_one_for_native_pixel_frames() {
assert!((1.0f32 - 1.0f32).abs() < 1e-6);
}
}
mod sck {
use super::{frame_from_cgimage_bytes, Frame};
use crate::errors::{CarDesktopError, Result};
use crate::models::DisplayId;
pub fn try_capture_display(display: DisplayId) -> Result<Option<Frame>> {
#[cfg(all(target_os = "macos", car_sck_swift_built))]
{
unsafe {
if ffi::car_sck_is_available() == 0 {
return Ok(None);
}
}
capture_via_sck(display).map(Some)
}
#[cfg(not(all(target_os = "macos", car_sck_swift_built)))]
{
let _ = display;
Ok(None)
}
}
#[cfg(all(target_os = "macos", car_sck_swift_built))]
fn capture_via_sck(display: DisplayId) -> Result<Frame> {
use std::ffi::CStr;
use std::ptr;
let display_id = display.0 as u32;
let mut buffer: *mut u8 = ptr::null_mut();
let mut len: usize = 0;
let mut width: u32 = 0;
let mut height: u32 = 0;
let mut bytes_per_row: usize = 0;
let mut err_msg: *mut std::os::raw::c_char = ptr::null_mut();
let rc = unsafe {
ffi::car_sck_capture_display(
display_id,
&mut buffer as *mut _,
&mut len as *mut _,
&mut width as *mut _,
&mut height as *mut _,
&mut bytes_per_row as *mut _,
&mut err_msg as *mut _,
)
};
if rc != 0 {
let detail = if err_msg.is_null() {
format!("SCK capture failed (rc={rc}, no error string)")
} else {
let s = unsafe { CStr::from_ptr(err_msg).to_string_lossy().into_owned() };
unsafe { ffi::car_sck_free_string(err_msg) };
format!("SCK capture failed (rc={rc}): {s}")
};
return Err(CarDesktopError::OsApi {
detail,
source: None,
});
}
if buffer.is_null() {
return Err(CarDesktopError::OsApi {
detail: "SCK returned rc=0 but null buffer".into(),
source: None,
});
}
let native = unsafe { std::slice::from_raw_parts(buffer, len) }.to_vec();
unsafe { ffi::car_sck_free_buffer(buffer, len) };
frame_from_cgimage_bytes(&native, width, height, bytes_per_row)
}
#[cfg(all(target_os = "macos", car_sck_swift_built))]
mod ffi {
use std::os::raw::c_char;
extern "C" {
pub fn car_sck_is_available() -> i32;
pub fn car_sck_free_string(ptr: *mut c_char);
pub fn car_sck_free_buffer(ptr: *mut u8, len: usize);
pub fn car_sck_capture_display(
display_id: u32,
out_buffer: *mut *mut u8,
out_len: *mut usize,
out_width: *mut u32,
out_height: *mut u32,
out_bytes_per_row: *mut usize,
out_err: *mut *mut c_char,
) -> i32;
}
}
}