#[allow(dead_code)]
pub enum Capture {
Rgba {
width: u32,
height: u32,
data: Vec<u8>,
},
Png(Vec<u8>),
}
pub fn capture_screens() -> Result<Vec<Capture>, String> {
#[cfg(target_os = "linux")]
{
linux::capture()
}
#[cfg(target_os = "macos")]
{
macos::capture()
}
#[cfg(target_os = "windows")]
{
windows_gdi::capture()
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
Err("screen capture isn't supported on this platform".into())
}
}
#[cfg(target_os = "linux")]
mod linux {
use super::Capture;
pub fn capture() -> Result<Vec<Capture>, String> {
if is_wayland() {
wayland_portal().map(|png| vec![Capture::Png(png)])
} else {
x11_grab().map(|c| vec![c])
}
}
fn is_wayland() -> bool {
std::env::var_os("WAYLAND_DISPLAY").is_some()
|| std::env::var("XDG_SESSION_TYPE").is_ok_and(|s| s.eq_ignore_ascii_case("wayland"))
}
fn x11_grab() -> Result<Capture, String> {
use x11rb::connection::Connection;
use x11rb::protocol::xproto::{ConnectionExt, ImageFormat};
let (conn, screen_num) =
x11rb::connect(None).map_err(|e| format!("cannot reach the X server: {e}"))?;
let screen = &conn.setup().roots[screen_num];
let (root, w, h) = (screen.root, screen.width_in_pixels, screen.height_in_pixels);
let reply = conn
.get_image(ImageFormat::Z_PIXMAP, root, 0, 0, w, h, !0)
.map_err(|e| format!("GetImage request failed: {e}"))?
.reply()
.map_err(|e| format!("GetImage failed: {e}"))?;
let data = zpixmap_to_rgba(&reply.data, w as u32, h as u32)?;
Ok(Capture::Rgba {
width: w as u32,
height: h as u32,
data,
})
}
fn zpixmap_to_rgba(src: &[u8], w: u32, h: u32) -> Result<Vec<u8>, String> {
let px = w as usize * h as usize;
if px == 0 {
return Err("X server reported a zero-sized screen".into());
}
let bpp = src.len() / px;
let mut out = vec![0u8; px * 4];
match bpp {
4 => {
for (o, s) in out.chunks_exact_mut(4).zip(src.chunks_exact(4)) {
o[0] = s[2]; o[1] = s[1]; o[2] = s[0]; o[3] = 255;
}
}
3 => {
for (o, s) in out.chunks_exact_mut(4).zip(src.chunks_exact(3)) {
o[0] = s[2];
o[1] = s[1];
o[2] = s[0];
o[3] = 255;
}
}
_ => {
return Err(format!(
"unsupported X image layout ({bpp} bytes/pixel); \
screenshot the QR to a file and import that instead"
));
}
}
Ok(out)
}
fn wayland_portal() -> Result<Vec<u8>, String> {
use ashpd::desktop::screenshot::Screenshot;
let uri = pollster::block_on(async {
Screenshot::request()
.interactive(false)
.modal(false)
.send()
.await
.map_err(|e| format!("screenshot portal request failed: {e}"))?
.response()
.map(|s| s.uri().as_str().to_owned())
.map_err(|e| format!("screenshot portal returned no image: {e}"))
})?;
let path = file_uri_to_path(&uri)?;
let bytes =
std::fs::read(&path).map_err(|e| format!("cannot read the portal screenshot: {e}"))?;
let _ = std::fs::remove_file(&path);
Ok(bytes)
}
fn file_uri_to_path(uri: &str) -> Result<std::path::PathBuf, String> {
let rest = uri
.strip_prefix("file://")
.ok_or_else(|| format!("portal returned a non-file URI: {uri}"))?;
let path_part = match rest.find('/') {
Some(i) => &rest[i..], None => return Err(format!("malformed file URI: {uri}")),
};
Ok(std::path::PathBuf::from(percent_decode(path_part)))
}
fn percent_decode(s: &str) -> String {
let b = s.as_bytes();
let mut out = Vec::with_capacity(b.len());
let mut i = 0;
while i < b.len() {
if b[i] == b'%' && i + 2 < b.len() {
if let (Some(hi), Some(lo)) = (hex_val(b[i + 1]), hex_val(b[i + 2])) {
out.push(hi * 16 + lo);
i += 3;
continue;
}
}
out.push(b[i]);
i += 1;
}
String::from_utf8_lossy(&out).into_owned()
}
fn hex_val(c: u8) -> Option<u8> {
match c {
b'0'..=b'9' => Some(c - b'0'),
b'a'..=b'f' => Some(c - b'a' + 10),
b'A'..=b'F' => Some(c - b'A' + 10),
_ => None,
}
}
}
#[cfg(target_os = "macos")]
mod macos {
use super::Capture;
pub fn capture() -> Result<Vec<Capture>, String> {
let path = std::env::temp_dir().join("keyroost-screengrab.png");
let status = std::process::Command::new("/usr/sbin/screencapture")
.args(["-x", "-t", "png"])
.arg(&path)
.status()
.map_err(|e| format!("could not run screencapture: {e}"))?;
if !status.success() {
return Err(
"screencapture failed — grant keyroost Screen Recording permission and retry"
.into(),
);
}
let read_result = std::fs::read(&path);
let _ = std::fs::remove_file(&path);
let bytes = read_result.map_err(|e| format!("cannot read the capture: {e}"))?;
Ok(vec![Capture::Png(bytes)])
}
}
#[cfg(target_os = "windows")]
mod windows_gdi {
use super::Capture;
pub fn capture() -> Result<Vec<Capture>, String> {
let frame = keyroost_screengrab::capture_virtual_screen()?;
Ok(vec![Capture::Rgba {
width: frame.width,
height: frame.height,
data: frame.rgba,
}])
}
}