//! 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 } }; let mut source_args = vec![ "ximagesrc".to_string(), // Full frames (no damage regions) to avoid partial-update artifacts; // use-damage=true is a later CPU optimization. show-pointer matches // Wayland's CursorMode::Embedded. "use-damage=false".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)) } } }