//! X11 capture: `ximagesrc` → the shared encode/mux tail in [`super::pipeline`]. //! Unlike Wayland there's no portal and no fd hand-off — `ximagesrc` opens its //! own X connection from `$DISPLAY`. The whole root window is captured by //! default; `--window` resolves a single window's XID via an `xwininfo` //! click-picker. The ticket is the access control, so capture starts silently //! when the first viewer connects (no host-side consent prompt). use anyhow::{Context, Result, bail}; use tokio::process::Command; use x11rb::connection::Connection; use x11rb::protocol::xproto::ConnectionExt; use super::pipeline::{self, CaptureHandle}; use super::quality::EffectiveQuality; use crate::cli::HostOpts; pub async fn start(opts: &HostOpts, quality: &EffectiveQuality) -> Result { let xid = if opts.window { Some(pick_window().await?) } else { None }; // Geometry mirrors Wayland's portal-handshake log line and feeds the // downscale presets (so they can compute an exact target size). A failure // here shouldn't abort capture — ximagesrc will surface a real error if the // X connection is genuinely unusable, and the scaler falls back to a // height-only negotiation when dims are unknown. let source_dims = match read_geometry(xid) { Ok((w, h)) => { tracing::info!(width = w, height = h, xid = ?xid, "X11 capture geometry"); Some((w as u32, h as u32)) } Err(e) => { tracing::warn!("could not read X11 geometry (capture will still try): {e:#}"); None } }; // XDamage capture (`use-damage=true`) only re-grabs changed screen // regions instead of copying the whole root window every frame. On a busy // desktop that is the difference between a usable framerate and ~1 fps — // `use-damage=false` does a full XGetImage per frame, which collapses on // servers without working MIT-SHM (and pins the CPU everywhere else). // Kept as the default; `PIXELPASS_X11_NO_DAMAGE=1` restores full-frame // capture if a driver produces partial-update artifacts with damage on. let use_damage = if std::env::var_os("PIXELPASS_X11_NO_DAMAGE").is_some() { "use-damage=false" } else { "use-damage=true" }; let mut source_args = vec![ "ximagesrc".to_string(), // show-pointer matches Wayland's CursorMode::Embedded. use_damage.to_string(), "show-pointer=true".to_string(), ]; if let Some(xid) = xid { source_args.push(format!("xid={xid}")); } // X11 has no leaked fd to clean up, so the post-spawn hook is a no-op. pipeline::spawn(opts, quality, source_dims, source_args, || {}).await } /// Run `xwininfo` and let the user click the window they want to share, then /// parse the `Window id: 0x…` line out of its output. Returns the numeric XID. async fn pick_window() -> Result { eprintln!("[pixelpass] click the window you want to share…"); let output = Command::new("xwininfo") .output() .await .context("failed to run `xwininfo` (install xorg-xwininfo)")?; if !output.status.success() { bail!( "xwininfo failed: {}", String::from_utf8_lossy(&output.stderr).trim() ); } let text = String::from_utf8_lossy(&output.stdout); for line in text.lines() { // e.g. "xwininfo: Window id: 0x3a00007 \"xterm\"" if let Some((_, rest)) = line.split_once("Window id: ") { let token = rest.split_whitespace().next().unwrap_or(""); let hex = token.strip_prefix("0x").unwrap_or(token); if let Ok(xid) = u32::from_str_radix(hex, 16) { return Ok(xid); } } } bail!("could not parse a window id from xwininfo output"); } /// Read pixel dimensions: the selected window's geometry when `--window` was /// used, otherwise the root window of the screen named by `$DISPLAY`. fn read_geometry(xid: Option) -> Result<(u16, u16)> { let (conn, screen_num) = x11rb::connect(None).context("could not connect to the X server (is DISPLAY set?)")?; match xid { Some(id) => { let geo = conn .get_geometry(id) .context("GetGeometry request failed")? .reply() .context("GetGeometry reply failed")?; Ok((geo.width, geo.height)) } None => { let screen = &conn.setup().roots[screen_num]; Ok((screen.width_in_pixels, screen.height_in_pixels)) } } }