use super::{
CaptureRequest, CaptureTarget, Display, Launched, Shot, ShotFormat, Window, launch_process,
};
use crate::extensions::computer::state::Rect;
use image::RgbaImage;
use std::time::{Duration, Instant};
pub fn displays() -> Result<Vec<Display>, String> {
let monitors = xcap::Monitor::all().map_err(|e| format!("xcap: {e}"))?;
let mut out = Vec::new();
for monitor in monitors {
out.push(Display {
name: monitor
.name()
.or_else(|_| monitor.friendly_name())
.unwrap_or_else(|_| "display".to_string()),
x: monitor.x().unwrap_or(0),
y: monitor.y().unwrap_or(0),
width: monitor.width().unwrap_or(0),
height: monitor.height().unwrap_or(0),
scale: f64::from(monitor.scale_factor().unwrap_or(1.0)),
primary: monitor.is_primary().unwrap_or(false),
});
}
if out.is_empty() {
return Err(
"xcap reported no monitor; on macOS this usually means the Screen Recording permission is missing"
.to_string(),
);
}
Ok(out)
}
pub fn windows() -> Result<Vec<Window>, String> {
let list = xcap::Window::all().map_err(|e| format!("xcap: {e}"))?;
let mut out = Vec::new();
for window in list {
if window.is_minimized().unwrap_or(false) {
continue;
}
let width = window.width().unwrap_or(0);
let height = window.height().unwrap_or(0);
if width == 0 || height == 0 {
continue;
}
out.push(Window {
id: u64::from(window.id().unwrap_or(0)),
title: window.title().unwrap_or_default(),
app: window.app_name().unwrap_or_default(),
pid: window.pid().unwrap_or(0),
x: window.x().unwrap_or(0),
y: window.y().unwrap_or(0),
width,
height,
focused: window.is_focused().unwrap_or(false),
});
}
Ok(out)
}
fn virtual_bounds() -> Result<Rect, String> {
let list = displays()?;
let x0 = list.iter().map(|d| d.x).min().unwrap_or(0);
let y0 = list.iter().map(|d| d.y).min().unwrap_or(0);
let x1 = list.iter().map(|d| d.x + d.width as i32).max().unwrap_or(0);
let y1 = list
.iter()
.map(|d| d.y + d.height as i32)
.max()
.unwrap_or(0);
Ok(Rect {
x: x0,
y: y0,
width: (x1 - x0).max(1) as u32,
height: (y1 - y0).max(1) as u32,
})
}
fn compose(bounds: Rect) -> Result<RgbaImage, String> {
let monitors = xcap::Monitor::all().map_err(|e| format!("xcap: {e}"))?;
let mut canvas = RgbaImage::new(bounds.width, bounds.height);
for monitor in monitors {
let (mx, my) = (monitor.x().unwrap_or(0), monitor.y().unwrap_or(0));
let pixels = monitor
.capture_image()
.map_err(|e| format!("xcap monitor capture: {e}"))?;
let offset_x = (mx - bounds.x) as i64;
let offset_y = (my - bounds.y) as i64;
for (px, py, pixel) in pixels.enumerate_pixels() {
let x = offset_x + i64::from(px);
let y = offset_y + i64::from(py);
if x >= 0 && y >= 0 && (x as u32) < bounds.width && (y as u32) < bounds.height {
canvas.put_pixel(x as u32, y as u32, *pixel);
}
}
}
Ok(canvas)
}
pub fn capture(req: &CaptureRequest) -> Result<Shot, String> {
let bounds = virtual_bounds()?;
let full = match &req.target {
CaptureTarget::Window(id) => {
let list = xcap::Window::all().map_err(|e| format!("xcap: {e}"))?;
let window = list
.into_iter()
.find(|w| u64::from(w.id().unwrap_or(0)) == *id)
.ok_or_else(|| format!("window {id} is not capturable"))?;
let origin = (window.x().unwrap_or(0), window.y().unwrap_or(0));
let image = window
.capture_image()
.map_err(|e| format!("xcap window {id}: {e}"))?;
return finish(image, origin, req, "window".to_string());
}
CaptureTarget::All => compose(bounds)?,
CaptureTarget::Display(name) => {
let monitors = xcap::Monitor::all().map_err(|e| format!("xcap: {e}"))?;
let monitor = monitors
.into_iter()
.find(|m| &m.name().unwrap_or_default() == name)
.ok_or_else(|| format!("no display named {name:?}"))?;
let origin = (monitor.x().unwrap_or(0), monitor.y().unwrap_or(0));
let image = monitor
.capture_image()
.map_err(|e| format!("xcap monitor {name}: {e}"))?;
return finish(image, origin, req, name.clone());
}
CaptureTarget::Region(rect) => {
let canvas = compose(bounds)?;
let x = (rect.x - bounds.x).max(0) as u32;
let y = (rect.y - bounds.y).max(0) as u32;
let w = rect.width.min(canvas.width().saturating_sub(x));
let h = rect.height.min(canvas.height().saturating_sub(y));
if w == 0 || h == 0 {
return Err(format!(
"the requested region ({},{} {}x{}) is outside the desktop",
rect.x, rect.y, rect.width, rect.height
));
}
let cropped = image::imageops::crop_imm(&canvas, x, y, w, h).to_image();
return finish(cropped, (rect.x, rect.y), req, "region".to_string());
}
};
finish(full, (bounds.x, bounds.y), req, "all displays".to_string())
}
fn finish(
image: RgbaImage,
origin: (i32, i32),
req: &CaptureRequest,
display: String,
) -> Result<Shot, String> {
let scale = if req.scale > 0.0 { req.scale } else { 1.0 };
let (width, height) = (
((image.width() as f64) * scale).round().max(1.0) as u32,
((image.height() as f64) * scale).round().max(1.0) as u32,
);
let scaled = if (width, height) == (image.width(), image.height()) {
image
} else {
image::imageops::resize(&image, width, height, image::imageops::FilterType::Triangle)
};
let mut bytes = Vec::new();
match req.format {
ShotFormat::Png => {
scaled
.write_to(
&mut std::io::Cursor::new(&mut bytes),
image::ImageFormat::Png,
)
.map_err(|e| format!("encode png: {e}"))?;
}
ShotFormat::Jpeg => {
let mut cursor = std::io::Cursor::new(&mut bytes);
let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(
&mut cursor,
req.jpeg_quality.clamp(1, 100),
);
encoder
.encode_image(&scaled)
.map_err(|e| format!("encode jpeg: {e}"))?;
}
}
Ok(Shot {
bytes,
format: req.format,
width: scaled.width(),
height: scaled.height(),
origin,
display,
})
}
pub fn launch(target: &str, args: &[String], wait_ms: u64) -> Result<Launched, String> {
let (pid, log) = launch_process(target, args)?;
let deadline = Instant::now() + Duration::from_millis(wait_ms.max(250));
let window = loop {
std::thread::sleep(Duration::from_millis(100));
if let Ok(list) = windows()
&& let Some(window) = list.into_iter().find(|w| w.pid == pid)
{
break Some(window);
}
if Instant::now() >= deadline {
break None;
}
};
Ok(Launched {
window,
pid,
log: log.map(|path| path.display().to_string()),
})
}