Skip to main content

sioul_sync/
colord.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2// Copyright © 2026 Aurélien Pierre
3
4//! colord's word on the screens, for the sites in the screen's own colours
5//! (sioul-app's colour.rs and cpp/screencolours.cpp; docs/colour.md, "Which
6//! atom is which screen"): the checksums of the profiles colord keeps for an
7//! output. Read only, over the system bus, on a connection made by
8//! `crate::bus` (Qt's own reached the bus through libdbus's registration and
9//! its process-wide locks: review of battery part C, G1), each question
10//! waited for 400 ms at most, as before.
11
12use dbus::Path;
13use dbus::blocking::LocalConnection;
14use dbus::blocking::stdintf::org_freedesktop_dbus::Properties;
15use std::collections::HashMap;
16use std::time::Duration;
17
18/// colord, on the system bus.
19const SERVICE: &str = "org.freedesktop.ColorManager";
20const MANAGER: &str = "/org/freedesktop/ColorManager";
21/// How long one question may take.
22const ASK: Duration = Duration::from_millis(400);
23/// How long the connection's setup may take: the window waits for it.
24const SETUP: Duration = Duration::from_secs(1);
25
26/// colord's checksums (MD5, lower case) of the profiles it keeps for the
27/// output X11 names `output` ("eDP-1"): its `ProfileId` (`"icc-<MD5>"`) and its
28/// metadata's `FILE_checksum`, profile by profile; none when colord does not
29/// know the output; nothing at all (`None`) when colord does not answer, or
30/// there is no system bus.
31pub fn checksums(output: &str) -> Option<Vec<String>> {
32    checksums_on(&crate::bus::system_address(), output)
33}
34
35/// `checksums`, from the colord on the bus at `address`.
36pub fn checksums_on(address: &str, output: &str) -> Option<Vec<String>> {
37    let bus = LocalConnection::from(crate::bus::connect_within(address, SETUP).ok()?);
38    let found: Result<(Path<'static>,), dbus::Error> = bus.with_proxy(SERVICE, MANAGER, ASK).method_call(SERVICE, "FindDeviceByProperty", ("XRANDR_name", output));
39    let device = match found {
40        Ok((device,)) => device,
41        // colord answered that it does not know this output; anything else: no colord.
42        Err(e) if e.name().is_some_and(|name| name.starts_with(SERVICE)) => return Some(Vec::new()),
43        Err(_) => return None,
44    };
45    let profiles: Vec<Path<'static>> = bus.with_proxy(SERVICE, device, ASK).get("org.freedesktop.ColorManager.Device", "Profiles").unwrap_or_default();
46    let mut sums = Vec::new();
47    for profile in profiles {
48        let profile = bus.with_proxy(SERVICE, profile, ASK);
49        if let Ok(id) = profile.get::<String>("org.freedesktop.ColorManager.Profile", "ProfileId")
50            && let Some(sum) = id.strip_prefix("icc-")
51        {
52            sums.push(sum.to_lowercase());
53        }
54        if let Ok(metadata) = profile.get::<HashMap<String, String>>("org.freedesktop.ColorManager.Profile", "Metadata")
55            && let Some(sum) = metadata.get("FILE_checksum").filter(|sum| !sum.is_empty())
56        {
57            sums.push(sum.to_lowercase());
58        }
59    }
60    Some(sums)
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    /// colord read as before (cpp/screencolours.cpp's own reading until
68    /// 13 October 2026), against a stand-in colord on the test's own bus (run
69    /// under dbus-run-session; skipped on a bus with a notification server,
70    /// the desktop's: nothing here may reach it): an output it knows, its
71    /// profiles' checksums in order, lower case; one it does not know, none;
72    /// no colord at all, nothing.
73    #[test]
74    fn colord_says_which_profiles_an_output_has() {
75        use dbus::arg::{RefArg, Variant};
76        use dbus::blocking::stdintf::org_freedesktop_dbus::RequestNameReply;
77        use dbus::channel::{MatchingReceiver, Sender as _};
78        use dbus::message::MatchRule;
79        let _alone = crate::bus::NOTIFICATION_SERVER.lock().unwrap_or_else(|e| e.into_inner());
80        let Some(address) = crate::bus::session_address() else {
81            eprintln!("No bus of the test's own (dbus-run-session): skipped.");
82            return;
83        };
84        let look = LocalConnection::from(crate::bus::connect(&address).unwrap());
85        let owned = |name: &str| -> bool { look.with_proxy("org.freedesktop.DBus", "/org/freedesktop/DBus", ASK).method_call("org.freedesktop.DBus", "NameHasOwner", (name,)).is_ok_and(|(owned,): (bool,)| owned) };
86        if owned("org.freedesktop.Notifications") || owned(SERVICE) {
87            eprintln!("A desktop's bus, or a colord on it already: skipped.");
88            return;
89        }
90        // No colord yet: nothing said.
91        assert_eq!(checksums_on(&address, "eDP-1"), None);
92        let (ready, started) = std::sync::mpsc::channel::<bool>();
93        let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
94        let stopped = std::sync::Arc::clone(&stop);
95        let served = address.clone();
96        std::thread::spawn(move || {
97            let Some(bus) = crate::bus::connect(&served).ok().map(LocalConnection::from).filter(|bus| matches!(bus.request_name(SERVICE, false, false, true), Ok(RequestNameReply::PrimaryOwner))) else {
98                let _ = ready.send(false);
99                return;
100            };
101            bus.start_receive(
102                MatchRule::new_method_call(),
103                Box::new(|call: dbus::Message, bus: &LocalConnection| {
104                    let path = call.path().map(|p| p.to_string()).unwrap_or_default();
105                    let reply = match (call.member().as_deref(), path.as_str()) {
106                        (Some("FindDeviceByProperty"), MANAGER) => {
107                            let (key, output): (String, String) = call.read2().unwrap();
108                            if key == "XRANDR_name" && output == "eDP-1" {
109                                call.method_return().append1(Path::from("/org/freedesktop/ColorManager/devices/xrandr_eDP_1"))
110                            } else {
111                                call.error(&"org.freedesktop.ColorManager.NotFound".into(), c"no such device")
112                            }
113                        }
114                        (Some("Get"), path) => {
115                            let (_interface, name): (String, String) = call.read2().unwrap();
116                            let value: Box<dyn RefArg> = match (path, name.as_str()) {
117                                ("/org/freedesktop/ColorManager/devices/xrandr_eDP_1", "Profiles") => Box::new(vec![Path::from("/org/freedesktop/ColorManager/profiles/a"), Path::from("/org/freedesktop/ColorManager/profiles/b")]),
118                                ("/org/freedesktop/ColorManager/profiles/a", "ProfileId") => Box::new("icc-0123456789ABCDEF0123456789ABCDEF".to_string()),
119                                ("/org/freedesktop/ColorManager/profiles/a", "Metadata") => Box::new(HashMap::from([("FILE_checksum".to_string(), "FEDCBA9876543210FEDCBA9876543210".to_string())])),
120                                ("/org/freedesktop/ColorManager/profiles/b", "ProfileId") => Box::new("custom-made".to_string()),
121                                ("/org/freedesktop/ColorManager/profiles/b", "Metadata") => Box::new(HashMap::<String, String>::new()),
122                                _ => return true,
123                            };
124                            call.method_return().append1(Variant(value))
125                        }
126                        _ => return true,
127                    };
128                    let _ = bus.send(reply);
129                    true
130                }),
131            );
132            let _ = ready.send(true);
133            let until = std::time::Instant::now() + Duration::from_secs(20);
134            while !stopped.load(std::sync::atomic::Ordering::Relaxed) && std::time::Instant::now() < until {
135                let _ = bus.process(Duration::from_millis(50));
136            }
137        });
138        if !started.recv_timeout(Duration::from_secs(5)).unwrap_or(false) {
139            eprintln!("colord's name taken on this bus: skipped.");
140            return;
141        }
142        assert_eq!(checksums_on(&address, "eDP-1"), Some(vec!["0123456789abcdef0123456789abcdef".to_string(), "fedcba9876543210fedcba9876543210".to_string()]));
143        assert_eq!(checksums_on(&address, "HDMI-1"), Some(Vec::new()), "an output colord does not know: none");
144        stop.store(true, std::sync::atomic::Ordering::Relaxed);
145    }
146}