#[cfg(not(target_os = "windows"))]
use std::path::Path;
pub fn system_display_icc() -> Option<String> {
env_override_icc().or_else(platform_display_icc)
}
fn env_override_icc() -> Option<String> {
match std::env::var("QUERCUS_DISPLAY_ICC") {
Ok(path) if !path.is_empty() => Some(path),
_ => None,
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MonitorRef {
MacDisplay(u32),
WinDevice(String),
X11Output(String),
}
pub fn system_display_icc_for(monitor: &MonitorRef) -> Option<String> {
env_override_icc()
.or_else(|| platform_display_icc_for(monitor))
.or_else(platform_display_icc)
}
#[cfg(target_os = "macos")]
fn platform_display_icc_for(monitor: &MonitorRef) -> Option<String> {
match monitor {
MonitorRef::MacDisplay(id) => macos::display_icc_for(*id),
_ => None,
}
}
#[cfg(target_os = "windows")]
fn platform_display_icc_for(monitor: &MonitorRef) -> Option<String> {
match monitor {
MonitorRef::WinDevice(device) => windows::display_icc_for(device),
_ => None,
}
}
#[cfg(target_os = "linux")]
fn platform_display_icc_for(monitor: &MonitorRef) -> Option<String> {
match monitor {
MonitorRef::X11Output(name) => linux::display_icc_for_output(name),
_ => None,
}
}
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
fn platform_display_icc_for(_monitor: &MonitorRef) -> Option<String> {
None
}
pub fn monitor_ref_from_fingerprint(fingerprint: &str) -> Option<MonitorRef> {
let (kind, value) = fingerprint.split_once(':')?;
match kind {
"mac" => {
let id: u32 = value.parse().ok()?;
Some(MonitorRef::MacDisplay(id))
}
"win" if !value.is_empty() => Some(MonitorRef::WinDevice(value.to_string())),
"x11" if !value.is_empty() => Some(MonitorRef::X11Output(value.to_string())),
_ => None,
}
}
#[cfg(target_os = "windows")]
pub fn windows_monitor_fingerprint(hmonitor: u64) -> Option<String> {
windows::monitor_device_name(hmonitor as usize).map(|name| format!("win:{name}"))
}
#[cfg(target_os = "linux")]
pub fn x11_monitor_fingerprint_at(x: f64, y: f64) -> Option<String> {
linux::x11_monitor_fingerprint_at(x, y)
}
#[cfg(target_os = "macos")]
fn platform_display_icc() -> Option<String> {
macos::display_icc()
}
#[cfg(target_os = "windows")]
fn platform_display_icc() -> Option<String> {
windows::display_icc()
}
#[cfg(target_os = "windows")]
pub fn windows_acm_active() -> bool {
windows::acm_active()
}
#[cfg(target_os = "linux")]
fn platform_display_icc() -> Option<String> {
linux::display_icc()
}
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
fn platform_display_icc() -> Option<String> {
None
}
#[cfg(not(target_os = "windows"))]
fn write_icc_cache(bytes: &[u8]) -> Option<String> {
if bytes.is_empty() {
return None;
}
let cache_root = crate::filefunctions::default_disk_cache_path();
let dir = Path::new(&cache_root).join("icc");
std::fs::create_dir_all(&dir).ok()?;
let path = dir.join(format!("{}.icc", crate::filefunctions::fnv1a_hex(bytes)));
if !path.exists() {
std::fs::write(&path, bytes).ok()?;
}
Some(path.to_string_lossy().into_owned())
}
#[cfg(any(target_os = "linux", test))]
fn parse_xprop_icc_hex(output: &str) -> Option<Vec<u8>> {
let after_eq = output.split('=').nth(1)?.to_ascii_lowercase();
let mut bytes = Vec::new();
let mut rest = after_eq.as_str();
while let Some(pos) = rest.find("0x") {
let digits_start = pos + 2;
let mut n_digits = 0;
for ch in rest[digits_start..].chars() {
if n_digits == 2 || !ch.is_ascii_hexdigit() {
break;
}
n_digits += 1;
}
if n_digits > 0 {
if let Ok(byte) = u8::from_str_radix(&rest[digits_start..digits_start + n_digits], 16) {
bytes.push(byte);
}
}
rest = &rest[digits_start + n_digits..];
}
if bytes.is_empty() {
None
} else {
Some(bytes)
}
}
#[cfg(any(target_os = "linux", test))]
#[derive(Clone, Debug, PartialEq, Eq)]
struct RandRMonitor {
x: i32,
y: i32,
width: i32,
height: i32,
outputs: Vec<String>,
}
#[cfg(any(target_os = "linux", test))]
fn parse_xrandr_monitors(output: &str) -> Vec<RandRMonitor> {
fn parse_geometry(token: &str) -> Option<(i32, i32, i32, i32)> {
let pieces: Vec<&str> = token.split(['+', '-']).collect();
if pieces.len() < 3 {
return None;
}
let dims = pieces[0].split_once('x')?;
let width: i32 = dims.0.split('/').next()?.parse().ok()?;
let height: i32 = dims.1.split('/').next()?.parse().ok()?;
let mut x: i32 = pieces[1].trim().parse().ok()?;
let mut y: i32 = pieces[2].trim().parse().ok()?;
let signs: Vec<u8> = token[pieces[0].len()..]
.bytes()
.filter(|b| matches!(b, b'+' | b'-'))
.collect();
if signs.first() == Some(&b'-') {
x = -x;
}
if signs.get(1) == Some(&b'-') {
y = -y;
}
Some((x, y, width, height))
}
let mut monitors = Vec::new();
for line in output.lines() {
let line = line.trim_start();
if line.starts_with("Monitors") {
continue;
}
let Some((_, rest)) = line.split_once(':') else {
continue;
};
let mut tokens = rest.split_whitespace();
if tokens.next().is_none() {
continue;
}
let Some(geometry) = tokens.next() else {
continue;
};
let Some((x, y, width, height)) = parse_geometry(geometry) else {
continue;
};
let outputs: Vec<String> = tokens.map(str::to_string).collect();
if outputs.is_empty() {
continue;
}
monitors.push(RandRMonitor {
x,
y,
width,
height,
outputs,
});
}
monitors
}
#[cfg(any(target_os = "linux", test))]
fn parse_output_icc_prop(output: &str, wanted: &str) -> Option<Vec<u8>> {
let mut in_section = false;
for line in output.lines() {
if line.starts_with(char::is_whitespace) {
if !in_section {
continue;
}
let (key, _) = line.trim().split_once('=')?;
if key.starts_with("_ICC_PROFILE") {
if let Some(bytes) = parse_xprop_icc_hex(line.trim()) {
return Some(bytes);
}
}
} else {
let name = line.split_whitespace().next().unwrap_or("");
in_section = name == wanted;
}
}
None
}
#[cfg(target_os = "macos")]
mod macos {
use std::ffi::c_void;
use super::write_icc_cache;
#[link(name = "CoreGraphics", kind = "framework")]
extern "C" {
fn CGMainDisplayID() -> u32;
fn CGDisplayCopyColorSpace(display: u32) -> *mut c_void;
fn CGColorSpaceCopyICCData(space: *const c_void) -> *mut c_void;
}
#[link(name = "CoreFoundation", kind = "framework")]
extern "C" {
fn CFDataGetLength(data: *const c_void) -> isize;
fn CFDataGetBytePtr(data: *const c_void) -> *const u8;
fn CFRelease(obj: *const c_void);
}
pub(super) fn display_icc() -> Option<String> {
display_icc_for(unsafe { CGMainDisplayID() })
}
pub(super) fn display_icc_for(display: u32) -> Option<String> {
let space = unsafe { CGDisplayCopyColorSpace(display) };
if space.is_null() {
return None;
}
let data = unsafe { CGColorSpaceCopyICCData(space) };
if data.is_null() {
unsafe { CFRelease(space) };
return None;
}
let len = unsafe { CFDataGetLength(data) };
let ptr = unsafe { CFDataGetBytePtr(data) };
let bytes = if len > 0 && !ptr.is_null() {
unsafe { std::slice::from_raw_parts(ptr, len as usize) }.to_vec()
} else {
Vec::new()
};
unsafe {
CFRelease(data);
CFRelease(space);
}
write_icc_cache(&bytes)
}
}
#[cfg(target_os = "windows")]
mod windows {
use std::ffi::c_void;
use std::path::Path;
#[link(name = "user32")]
extern "system" {
fn GetDC(hwnd: *const c_void) -> *mut c_void;
fn ReleaseDC(hwnd: *const c_void, hdc: *mut c_void) -> i32;
fn GetMonitorInfoW(monitor: *const c_void, info: *mut c_void) -> i32;
}
#[link(name = "gdi32")]
extern "system" {
fn GetICMProfileW(hdc: *mut c_void, name_len: *mut u32, name: *mut u16) -> i32;
fn CreateDCW(
driver: *const u16,
device: *const u16,
port: *const c_void,
dev_mode: *const c_void,
) -> *mut c_void;
fn DeleteDC(hdc: *mut c_void) -> i32;
}
#[link(name = "ntdll")]
extern "system" {
fn RtlGetVersion(info: *mut OsVersionInfo) -> i32;
}
#[link(name = "advapi32")]
extern "system" {
fn RegOpenKeyExW(
key: *const c_void,
sub_key: *const u16,
options: u32,
desired: u32,
result: *mut *mut c_void,
) -> i32;
fn RegQueryValueExW(
key: *mut c_void,
value_name: *const u16,
reserved: *const u32,
value_type: *mut u32,
data: *mut u8,
data_len: *mut u32,
) -> i32;
fn RegCloseKey(key: *mut c_void) -> i32;
}
#[repr(C)]
struct OsVersionInfo {
length: u32,
major_version: u32,
minor_version: u32,
build_number: u32,
platform_id: u32,
csd_version: [u16; 128],
}
#[repr(C)]
struct Rect {
left: i32,
top: i32,
right: i32,
bottom: i32,
}
#[repr(C)]
struct MonitorInfoExW {
cb_size: u32,
rc_monitor: Rect,
rc_work: Rect,
dw_flags: u32,
sz_device: [u16; 32],
}
const HKEY_CURRENT_USER: *const c_void = 0x8000_0001usize as *const c_void;
const KEY_READ: u32 = 0x2_0019;
const REG_DWORD: u32 = 4;
const ERROR_SUCCESS: i32 = 0;
const BUILD_WINDOWS_11: u32 = 22000;
const BUILD_WIN11_24H2: u32 = 26100;
fn wide_nul(s: &str) -> Vec<u16> {
let mut wide: Vec<u16> = s.encode_utf16().collect();
wide.push(0);
wide
}
fn windows_build() -> u32 {
let mut info = OsVersionInfo {
length: std::mem::size_of::<OsVersionInfo>() as u32,
major_version: 0,
minor_version: 0,
build_number: 0,
platform_id: 0,
csd_version: [0; 128],
};
let status = unsafe { RtlGetVersion(&mut info) };
if status == 0 {
info.build_number
} else {
0
}
}
fn acm_registry_flag() -> Option<bool> {
let sub_key = wide_nul(r"Software\Microsoft\Windows\CurrentVersion\VideoSettings");
let value_name = wide_nul("EnableAutoColorManagement");
let mut key: *mut c_void = std::ptr::null_mut();
let status =
unsafe { RegOpenKeyExW(HKEY_CURRENT_USER, sub_key.as_ptr(), 0, KEY_READ, &mut key) };
if status != ERROR_SUCCESS {
return None;
}
let mut data = [0u8; 4];
let mut len = data.len() as u32;
let mut kind = 0u32;
let status = unsafe {
RegQueryValueExW(
key,
value_name.as_ptr(),
std::ptr::null(),
&mut kind,
data.as_mut_ptr(),
&mut len,
)
};
unsafe { RegCloseKey(key) };
if status != ERROR_SUCCESS || kind != REG_DWORD || len != 4 {
return None;
}
Some(u32::from_le_bytes(data) != 0)
}
pub(super) fn acm_active() -> bool {
match std::env::var("QUERCUS_WINDOWS_COLOR") {
Ok(v) if v.eq_ignore_ascii_case("os") => return true,
Ok(v) if v.eq_ignore_ascii_case("self") => return false,
_ => {}
}
let build = windows_build();
if build < BUILD_WINDOWS_11 {
return false;
}
match acm_registry_flag() {
Some(flag) => flag,
None => build >= BUILD_WIN11_24H2,
}
}
pub(super) fn display_icc() -> Option<String> {
let hdc = unsafe { GetDC(std::ptr::null()) };
if hdc.is_null() {
return None;
}
let profile = icm_profile_for_hdc(hdc);
unsafe { ReleaseDC(std::ptr::null(), hdc) };
profile
}
pub(super) fn display_icc_for(device: &str) -> Option<String> {
let name = wide_nul(device);
let hdc = unsafe {
CreateDCW(
name.as_ptr(),
name.as_ptr(),
std::ptr::null(),
std::ptr::null(),
)
};
if hdc.is_null() {
return None;
}
let profile = icm_profile_for_hdc(hdc);
unsafe { DeleteDC(hdc) };
profile
}
pub(super) fn monitor_device_name(hmonitor: usize) -> Option<String> {
let mut info = MonitorInfoExW {
cb_size: std::mem::size_of::<MonitorInfoExW>() as u32,
rc_monitor: Rect {
left: 0,
top: 0,
right: 0,
bottom: 0,
},
rc_work: Rect {
left: 0,
top: 0,
right: 0,
bottom: 0,
},
dw_flags: 0,
sz_device: [0; 32],
};
let ok = unsafe {
GetMonitorInfoW(
hmonitor as *const c_void,
&mut info as *mut _ as *mut c_void,
)
};
if ok == 0 {
return None;
}
let end = info
.sz_device
.iter()
.position(|&u| u == 0)
.unwrap_or(info.sz_device.len());
let name = String::from_utf16_lossy(&info.sz_device[..end]);
if name.is_empty() {
None
} else {
Some(name)
}
}
fn icm_profile_for_hdc(hdc: *mut c_void) -> Option<String> {
let mut len: u32 = 0;
let ok = unsafe { GetICMProfileW(hdc, &mut len, std::ptr::null_mut()) };
if ok == 0 || len == 0 {
return None;
}
let mut buf = vec![0u16; len as usize + 1];
let ok = unsafe { GetICMProfileW(hdc, &mut len, buf.as_mut_ptr()) };
if ok == 0 {
return None;
}
let path = String::from_utf16_lossy(&buf[..len as usize]);
let path = path.trim_end_matches('\0');
if path.is_empty() || !Path::new(path).is_file() {
return None;
}
Some(path.to_string())
}
}
#[cfg(target_os = "linux")]
mod linux {
use std::io::Read;
use std::process::{Command, Stdio};
use std::sync::Mutex;
use std::thread;
use std::time::{Duration, Instant};
use super::*;
pub(super) fn display_icc() -> Option<String> {
for prop in ["_ICC_PROFILE", "_ICC_PROFILE_1", "_ICC_PROFILE_2"] {
let mut cmd = Command::new("xprop");
cmd.args(["-root", prop]);
let Some(out) = run_capture(&mut cmd, Duration::from_secs(2)) else {
continue;
};
let Some(bytes) = parse_xprop_icc_hex(&String::from_utf8_lossy(&out)) else {
continue;
};
if let Some(path) = write_icc_cache(&bytes) {
return Some(path);
}
}
colord_icc_path()
}
pub(super) fn display_icc_for_output(output: &str) -> Option<String> {
let mut cmd = Command::new("xrandr");
cmd.args(["--prop"]);
let out = run_capture(&mut cmd, Duration::from_secs(2))?;
let text = String::from_utf8_lossy(&out);
let bytes = parse_output_icc_prop(&text, output)?;
write_icc_cache(&bytes)
}
static MONITORS_CACHE: Mutex<Option<(Instant, Vec<RandRMonitor>)>> = Mutex::new(None);
const MONITORS_TTL: Duration = Duration::from_secs(30);
fn monitors_snapshot() -> Vec<RandRMonitor> {
let mut guard = MONITORS_CACHE.lock().unwrap_or_else(|e| e.into_inner());
if let Some((stamp, monitors)) = guard.as_ref() {
if stamp.elapsed() < MONITORS_TTL {
return monitors.clone();
}
}
let monitors = fetch_monitors();
*guard = Some((Instant::now(), monitors.clone()));
monitors
}
fn fetch_monitors() -> Vec<RandRMonitor> {
let mut cmd = Command::new("xrandr");
cmd.args(["--listmonitors"]);
match run_capture(&mut cmd, Duration::from_secs(2)) {
Some(out) => parse_xrandr_monitors(&String::from_utf8_lossy(&out)),
None => Vec::new(),
}
}
pub(super) fn x11_monitor_fingerprint_at(x: f64, y: f64) -> Option<String> {
let monitors = monitors_snapshot();
let px = x.round() as i32;
let py = y.round() as i32;
for monitor in &monitors {
if px >= monitor.x
&& px < monitor.x + monitor.width
&& py >= monitor.y
&& py < monitor.y + monitor.height
{
if let Some(output) = monitor.outputs.first() {
return Some(format!("x11:{output}"));
}
}
}
None
}
fn run_capture(cmd: &mut Command, timeout: Duration) -> Option<Vec<u8>> {
let mut child = cmd
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.ok()?;
let deadline = Instant::now() + timeout;
loop {
match child.try_wait() {
Ok(Some(status)) => {
if !status.success() {
return None;
}
break;
}
Ok(None) => {
if Instant::now() >= deadline {
let _ = child.kill();
let _ = child.wait();
return None;
}
thread::sleep(Duration::from_millis(10));
}
Err(_) => return None,
}
}
let mut out = Vec::new();
let _ = child.stdout.take()?.read_to_end(&mut out);
if out.is_empty() {
None
} else {
Some(out)
}
}
fn colord_icc_path() -> Option<String> {
let device = colormgr_value(&["get-default-device"])?;
let profile = colormgr_value(&["device-get-default-profile", &device])?;
let mut cmd = Command::new("colormgr");
cmd.args(["get-profile", &profile]);
let out = run_capture(&mut cmd, Duration::from_secs(2))?;
let text = String::from_utf8_lossy(&out);
let line = text
.lines()
.map(str::trim_start)
.find(|l| l.starts_with("Filename:"))?;
let name = line.split_once(':')?.1.trim();
if name.is_empty() {
None
} else {
Some(name.to_string())
}
}
fn colormgr_value(args: &[&str]) -> Option<String> {
let mut cmd = Command::new("colormgr");
cmd.args(args);
let out = run_capture(&mut cmd, Duration::from_secs(2))?;
let text = String::from_utf8_lossy(&out);
let value = text
.split_whitespace()
.find(|t| t.starts_with("/org/freedesktop/ColorManager/"))
.or_else(|| text.lines().map(str::trim).find(|l| !l.is_empty()))
.unwrap_or("");
if value.is_empty() {
None
} else {
Some(value.to_string())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Mutex, MutexGuard};
fn env_lock() -> &'static Mutex<()> {
crate::test_support::env_lock()
}
fn unique_temp_dir(tag: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!(
"quercus-displayicc-{}-{}-{}",
tag,
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn parse_xprop_hex_normal() {
let out = "_ICC_PROFILE(8)\t= 0x3c, 0x6f, 0x6f, 0x0";
assert_eq!(parse_xprop_icc_hex(out), Some(vec![0x3c, 0x6f, 0x6f, 0x0]));
}
#[test]
fn parse_xprop_hex_suffixed_name() {
let out = "_ICC_PROFILE_1(8)\t= 0x00, 0x01";
assert_eq!(parse_xprop_icc_hex(out), Some(vec![0x00, 0x01]));
}
#[test]
fn parse_xprop_hex_empty_and_absent() {
assert_eq!(parse_xprop_icc_hex("xprop: error: Invalid atom"), None);
assert_eq!(parse_xprop_icc_hex("_ICC_PROFILE(8)\t= "), None);
assert_eq!(parse_xprop_icc_hex(""), None);
}
#[test]
fn parse_xprop_hex_malformed() {
assert_eq!(
parse_xprop_icc_hex("_ICC_PROFILE(8)\t= 0xZZ, 0x3c, 0xGG"),
Some(vec![0x3c])
);
assert_eq!(parse_xprop_icc_hex("_ICC_PROFILE(8)\t= 0x"), None);
assert_eq!(parse_xprop_icc_hex("0x3c 0x6f"), None);
assert_eq!(
parse_xprop_icc_hex("_ICC_PROFILE(8)\t= 0X3C, 0X6f"),
Some(vec![0x3c, 0x6f])
);
assert_eq!(parse_xprop_icc_hex("_ICC_PROFILE(8)\t= 3c, 6f"), None);
}
#[test]
fn env_override_wins() {
let _guard: MutexGuard<()> = env_lock().lock().unwrap();
let dir = unique_temp_dir("override");
let profile = dir.join("fake.icc");
std::fs::write(&profile, b"fake").unwrap();
std::env::set_var("QUERCUS_DISPLAY_ICC", &profile);
let got = system_display_icc();
std::env::remove_var("QUERCUS_DISPLAY_ICC");
assert_eq!(got, Some(profile.to_string_lossy().into_owned()));
}
#[test]
fn empty_override_falls_through() {
let _guard: MutexGuard<()> = env_lock().lock().unwrap();
std::env::set_var("QUERCUS_DISPLAY_ICC", "");
let with_empty = system_display_icc();
std::env::remove_var("QUERCUS_DISPLAY_ICC");
let without = system_display_icc();
assert_eq!(with_empty, without);
}
#[test]
fn platform_query_does_not_panic() {
if let Some(path) = system_display_icc() {
assert!(!path.is_empty());
}
}
#[cfg(not(target_os = "windows"))]
#[test]
fn icc_cache_roundtrip_and_dedup() {
let _guard: MutexGuard<()> = env_lock().lock().unwrap();
let dir = unique_temp_dir("cache");
std::env::set_var("QUERCUS_CONFIG_DIR", &dir);
let bytes = b"\xacsp sRGB IEC61966-2.1 profile bytes".to_vec();
let p1 = write_icc_cache(&bytes).expect("cache write");
let p2 = write_icc_cache(&bytes).expect("cache write");
assert_eq!(p1, p2, "identical content reuses the same file");
assert_eq!(std::fs::read(&p1).unwrap(), bytes);
assert!(p1.ends_with(".icc"));
let stem = Path::new(&p1).file_stem().unwrap().to_str().unwrap();
assert_eq!(stem.len(), 16);
assert!(stem.chars().all(|c| c.is_ascii_hexdigit()));
assert!(Path::new(&p1).starts_with(&dir));
let p3 = write_icc_cache(b"other bytes").unwrap();
assert_ne!(p1, p3);
std::env::remove_var("QUERCUS_CONFIG_DIR");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn parse_xrandr_monitors_two_screens() {
let out = "\
Monitors: 2
0: +*eDP-1 1920/344x1080/194+0+0 eDP-1
1: +HDMI-1 3840/600x2160/340+1920+0 HDMI-1
";
let monitors = parse_xrandr_monitors(out);
assert_eq!(monitors.len(), 2);
assert_eq!(monitors[0].x, 0);
assert_eq!(monitors[0].y, 0);
assert_eq!(monitors[0].width, 1920);
assert_eq!(monitors[0].height, 1080);
assert_eq!(monitors[0].outputs, ["eDP-1"]);
assert_eq!(monitors[1].x, 1920);
assert_eq!(monitors[1].y, 0);
assert_eq!(monitors[1].width, 3840);
assert_eq!(monitors[1].height, 2160);
assert_eq!(monitors[1].outputs, ["HDMI-1"]);
}
#[test]
fn parse_xrandr_monitors_negative_coords_and_spanning() {
let out = "\
Monitors: 2
0: +*DP-1 1080/293x1920/509+0+0 DP-1
1: +DP-2 1920/509x1080/293-1080+0 DP-2
";
let monitors = parse_xrandr_monitors(out);
assert_eq!(monitors[1].x, -1080);
assert_eq!(monitors[1].y, 0);
let out = "\
Monitors: 1
0: +*eDP-1 3840/700x1080/194+0+0 eDP-1 HDMI-1
";
let monitors = parse_xrandr_monitors(out);
assert_eq!(monitors.len(), 1);
assert_eq!(monitors[0].outputs, ["eDP-1", "HDMI-1"]);
}
#[test]
fn parse_xrandr_monitors_malformed() {
assert_eq!(parse_xrandr_monitors(""), Vec::<RandRMonitor>::new());
assert_eq!(
parse_xrandr_monitors("Monitors: 0\n"),
Vec::<RandRMonitor>::new()
);
let out = "\
Monitors: 2
garbage line
0: +*eDP-1 eDP-1
1: +HDMI-1 1920/344x1080/194+1920+0
2: +VGA-1 1024/200x768/150+0+0 VGA-1
";
let monitors = parse_xrandr_monitors(out);
assert_eq!(monitors.len(), 1);
assert_eq!(monitors[0].outputs, ["VGA-1"]);
}
#[test]
fn parse_output_icc_prop_hit_and_miss() {
let out = "\
Screen 0: minimum 320 x 200, current 3840 x 1080, maximum 16384 x 16384
eDP-1 connected primary 1920x1080+0+0 (normal left inverted right x axis y axis) 344mm x 194mm
\t_ICC_PROFILE(8)\t= 0x3c, 0x6f, 0x6f
\tEDID(0)\t= 0x00, 0xff, 0xff, 0xff
HDMI-1 connected 1920x1080+1920+0 (normal left inverted right x axis y axis) 600mm x 340mm
\t_ICC_PROFILE(8)\t= 0x41, 0x42
";
assert_eq!(
parse_output_icc_prop(out, "eDP-1"),
Some(vec![0x3c, 0x6f, 0x6f])
);
assert_eq!(parse_output_icc_prop(out, "HDMI-1"), Some(vec![0x41, 0x42]));
assert_eq!(parse_output_icc_prop(out, "VGA-1"), None);
}
#[test]
fn parse_output_icc_prop_without_profile() {
let out = "\
DP-1 connected 1920x1080+0+0 (normal left inverted right x axis y axis) 344mm x 194mm
\tEDID(0)\t= 0x00, 0xff
";
assert_eq!(parse_output_icc_prop(out, "DP-1"), None);
let out = "\
DP-1 connected 1920x1080+0+0 (normal left inverted right x axis y axis) 344mm x 194mm
\t_ICC_PROFILE(8)\t= 0x
";
assert_eq!(parse_output_icc_prop(out, "DP-1"), None);
}
#[test]
fn monitor_ref_fingerprint_roundtrip() {
assert_eq!(
monitor_ref_from_fingerprint("mac:4294967295"),
Some(MonitorRef::MacDisplay(u32::MAX))
);
assert_eq!(
monitor_ref_from_fingerprint(r"win:\\.\DISPLAY1"),
Some(MonitorRef::WinDevice(r"\\.\DISPLAY1".to_string()))
);
assert_eq!(
monitor_ref_from_fingerprint("x11:eDP-1"),
Some(MonitorRef::X11Output("eDP-1".to_string()))
);
assert_eq!(monitor_ref_from_fingerprint(""), None);
assert_eq!(monitor_ref_from_fingerprint("nope"), None);
assert_eq!(monitor_ref_from_fingerprint("mac:abc"), None);
assert_eq!(monitor_ref_from_fingerprint("win:"), None);
assert_eq!(monitor_ref_from_fingerprint("x11:"), None);
assert_eq!(monitor_ref_from_fingerprint("os:whatever"), None);
}
}