//! TRACES: FR-DSP-8 | FR-PLAT-LIN-2 //! Acquisition on X11: ICC profiles as root-window properties. //! //! The mechanism is the ICC Profiles in X specification, and it predates //! everything else in this module by two decades. A colour manager — colord //! under GNOME, `xiccd`, `xcalib`, or the desktop's own — loads the profile //! for each output and publishes its bytes on the *root window* as a property: //! `_ICC_PROFILE` for the first output, `_ICC_PROFILE_1`, `_ICC_PROFILE_2` and //! so on for the rest, numbered by the output's position in RandR's list. //! //! Any client can read them, which is exactly why this works and exactly why //! [`super::icc::read_profile`] validates so carefully: a root-window property //! is untyped shared state that anything on the display may write. //! //! # Geometry comes from RandR, and it is what makes a move detectable //! //! X11 will also say where each output sits in the desktop's coordinate space, //! and will tell a client where its own window is. Those two facts together //! are the whole of FR-DSP-8's "updates when the window moves between //! displays" on this display server — the window's centre falls in one //! rectangle or another, and the answer changes as it is dragged. Wayland //! gives neither, which is why `wayland.rs` says what it says. use x11rb::connection::Connection; use x11rb::protocol::randr::ConnectionExt as RandrExt; use x11rb::protocol::xproto::{AtomEnum, ConnectionExt as XExt}; use super::{ nearest_by_colorants, Bounds, DisplayInfo, DisplayProfile, FallbackReason, ProfileSource, }; /// Every output the X server has, with whatever profile is published for it. pub fn probe() -> Result, FallbackReason> { let (conn, screen_num) = x11rb::connect(None).map_err(|e| FallbackReason::NoConnection(format!("X11: {e}")))?; let root = conn .setup() .roots .get(screen_num) .ok_or_else(|| FallbackReason::NoConnection("X11: no screen".to_string()))? .root; // RandR describes the outputs. Its absence is survivable: a server without // it still has a root window with `_ICC_PROFILE` on it, so fall through to // a single unnamed display carrying the first property rather than giving // up on colour management entirely. let outputs = enumerate(&conn, root).unwrap_or_default(); let outputs = if outputs.is_empty() { vec![(String::from("display"), None)] } else { outputs }; Ok(outputs .into_iter() .enumerate() .map(|(index, (name, bounds))| DisplayInfo { name, bounds, profile: profile_for(&conn, root, index), }) .collect()) } /// An output's connector name and the rectangle it occupies, where RandR /// would say. Named so that the order of the two is fixed in one place. type Output = (String, Option); /// The connected outputs, in RandR order, with their desktop rectangles. /// /// Order is load-bearing rather than cosmetic: it is what numbers the /// `_ICC_PROFILE_` properties, so reordering this list would hand every /// monitor its neighbour's profile. fn enumerate(conn: &impl Connection, root: u32) -> Result, Box> { let resources = conn.randr_get_screen_resources_current(root)?.reply()?; let stamp = resources.config_timestamp; let mut found = Vec::new(); for output in resources.outputs { let Ok(info) = conn.randr_get_output_info(output, stamp)?.reply() else { continue; }; // `crtc == 0` is an output with no CRTC driving it: a port with // nothing plugged in, or a disabled monitor. It occupies no part of // the desktop and takes no place in the `_ICC_PROFILE_` numbering. if info.crtc == 0 { continue; } let name = String::from_utf8_lossy(&info.name).to_string(); let bounds = conn .randr_get_crtc_info(info.crtc, stamp) .ok() .and_then(|c| c.reply().ok()) .map(|c| Bounds { x: i32::from(c.x), y: i32::from(c.y), width: u32::from(c.width), height: u32::from(c.height), }); found.push((name, bounds)); } Ok(found) } /// The property this output's profile would be published on, read and reduced. fn profile_for(conn: &impl Connection, root: u32, index: usize) -> DisplayProfile { // The specification's numbering: the first output's property carries no // suffix. Writing `_ICC_PROFILE_0` instead reads nothing on every desktop. let name = if index == 0 { "_ICC_PROFILE".to_string() } else { format!("_ICC_PROFILE_{index}") }; let bytes = match fetch(conn, root, &name) { Ok(Some(bytes)) => bytes, // No property. The overwhelmingly common case on a stock desktop with // no calibration installed, and not a fault of any kind. Ok(None) => return DisplayProfile::fallback(FallbackReason::NoProfileForDisplay), Err(e) => return DisplayProfile::fallback(FallbackReason::Unreadable(format!("X11: {e}"))), }; match super::icc::read_profile(&bytes) { Ok(summary) => { let (space, exact) = nearest_by_colorants(&summary.colorants); DisplayProfile { space, source: ProfileSource::X11RootProperty(name), described_as: summary.description, approximated: !exact, } } Err(why) => DisplayProfile::fallback(FallbackReason::Unreadable(why)), } } /// Read a root-window property whole. /// /// The length is asked for in 32-bit units, which is X11's unit for this call /// and a trap worth naming: a display profile is a few kilobytes and a request /// stated in bytes would truncate it into a shape `read_profile` then rejects /// as corrupt. `u32::MAX / 4` asks for all of it. fn fetch( conn: &impl Connection, root: u32, name: &str, ) -> Result>, Box> { let atom = conn.intern_atom(true, name.as_bytes())?.reply()?.atom; // `only_if_exists` above: an atom that no one has interned is a property // no one has set, and interning it ourselves would litter the server with // atoms for the monitors this desktop does not have. if atom == 0 { return Ok(None); } let reply = conn .get_property(false, root, atom, AtomEnum::CARDINAL, 0, u32::MAX / 4)? .reply()?; if reply.value.is_empty() { return Ok(None); } Ok(Some(reply.value)) }