#[allow(dead_code)]
pub fn parse_monitor_name_from_edid(edid: &[u8]) -> Option<String> {
if edid.len() < 128 {
return None;
}
let offsets = [54, 72, 90, 108];
for &offset in &offsets {
if offset + 18 <= edid.len() {
let block = &edid[offset..offset + 18];
if block[0] == 0x00 && block[1] == 0x00 && block[2] == 0x00 && block[3] == 0xFC {
let name_bytes = &block[4..17];
let name = String::from_utf8_lossy(name_bytes);
let cleaned = name.trim().replace('\0', "").to_string();
if !cleaned.is_empty() {
return Some(cleaned);
}
}
}
}
None
}
#[allow(dead_code)]
pub fn parse_refresh_rate_from_edid(edid: &[u8]) -> Option<f64> {
if edid.len() < 72 {
return None;
}
let block = &edid[54..72];
let pixel_clock = ((block[1] as u32) << 8) | (block[0] as u32);
if pixel_clock == 0 {
return None;
}
let pixel_clock_hz = pixel_clock * 10_000;
let h_active = (block[2] as u32) | (((block[4] as u32) & 0xF0) << 4);
let h_blanking = (block[3] as u32) | (((block[4] as u32) & 0x0F) << 8);
let v_active = (block[5] as u32) | (((block[7] as u32) & 0xF0) << 4);
let v_blanking = (block[6] as u32) | (((block[7] as u32) & 0x0F) << 8);
let h_total = h_active + h_blanking;
let v_total = v_active + v_blanking;
if h_total == 0 || v_total == 0 {
return None;
}
let refresh = (pixel_clock_hz as f64) / ((h_total * v_total) as f64);
Some((refresh * 100.0).round() / 100.0)
}
#[allow(dead_code)]
pub fn format_refresh_rate(refresh: f64) -> String {
if (refresh - refresh.round()).abs() < 0.01 {
format!("{:.0}", refresh)
} else {
format!("{:.2}", refresh)
}
}
#[allow(dead_code)]
pub fn parse_serial_number_from_edid(edid: &[u8]) -> Option<String> {
if edid.len() < 128 {
return None;
}
let offsets = [54, 72, 90, 108];
for &offset in &offsets {
if offset + 18 <= edid.len() {
let block = &edid[offset..offset + 18];
if block[0] == 0x00 && block[1] == 0x00 && block[2] == 0x00 && block[3] == 0xFF {
let serial_bytes = &block[4..17];
let serial = String::from_utf8_lossy(serial_bytes);
let cleaned = serial.trim().replace('\0', "").to_string();
if !cleaned.is_empty() {
return Some(cleaned);
}
}
}
}
let serial_num = ((edid[15] as u32) << 24)
| ((edid[14] as u32) << 16)
| ((edid[13] as u32) << 8)
| (edid[12] as u32);
if serial_num != 0 && serial_num != 0xFFFFFFFF {
return Some(serial_num.to_string());
}
None
}
#[allow(dead_code)]
pub fn get_monitor_name_for_port(port: &str) -> Option<String> {
if let Ok(entries) = std::fs::read_dir("/sys/class/drm") {
for entry in entries.filter_map(|e| e.ok()) {
let name = entry.file_name().to_string_lossy().to_string();
if name.ends_with(port) {
let edid_path = entry.path().join("edid");
if edid_path.exists() {
if let Ok(edid_bytes) = std::fs::read(&edid_path) {
if let Some(monitor_name) = parse_monitor_name_from_edid(&edid_bytes) {
return Some(monitor_name);
}
}
}
}
}
}
None
}
pub fn detect_displays() -> Vec<String> {
#[cfg(target_os = "macos")]
{
crate::macos_ffi::get_displays()
}
#[cfg(target_os = "windows")]
{
#[repr(C)]
struct DisplayDevice {
cb: u32,
device_name: [u16; 32],
device_string: [u16; 128],
state_flags: u32,
device_id: [u16; 128],
device_key: [u16; 128],
}
#[repr(C)]
struct DevMode {
device_name: [u16; 32], spec_version: u16, driver_version: u16, size: u16, driver_extra: u16, fields: u32, position_x: i32, position_y: i32, display_orientation: u32, display_fixed_output: u32, color: u16, duplex: u16, y_resolution: u16, tt_option: u16, collate: u16, form_name: [u16; 32], log_pixels: u16, bits_per_pel: u32, pels_width: u32, pels_height: u32, display_flags: u32, display_frequency: u32, icm_method: u32, icm_intent: u32, media_type: u32, dither_type: u32, reserved1: u32, reserved2: u32, panning_width: u32, panning_height: u32, }
#[link(name = "user32")]
extern "system" {
fn EnumDisplayDevicesW(
lpDevice: *const u16,
iDevNum: u32,
lpDisplayDevice: *mut DisplayDevice,
dwFlags: u32,
) -> i32;
fn EnumDisplaySettingsW(
lpszDeviceName: *const u16,
iModeNum: u32,
lpDevMode: *mut DevMode,
) -> i32;
}
const DISPLAY_DEVICE_ACTIVE: u32 = 0x00000001;
const ENUM_CURRENT_SETTINGS: u32 = 0xFFFF_FFFF;
fn u16_to_string(buf: &[u16]) -> String {
let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
String::from_utf16_lossy(&buf[..len])
}
let mut displays = Vec::new();
let mut dev_num = 0u32;
loop {
let mut dd = DisplayDevice {
cb: std::mem::size_of::<DisplayDevice>() as u32,
device_name: [0u16; 32],
device_string: [0u16; 128],
state_flags: 0,
device_id: [0u16; 128],
device_key: [0u16; 128],
};
let ok = unsafe { EnumDisplayDevicesW(std::ptr::null(), dev_num, &mut dd, 0) };
if ok == 0 {
break;
}
dev_num += 1;
if dd.state_flags & DISPLAY_DEVICE_ACTIVE == 0 {
continue;
}
let adapter_name = u16_to_string(&dd.device_string);
let mut dm = unsafe { std::mem::zeroed::<DevMode>() };
dm.size = std::mem::size_of::<DevMode>() as u16;
let settings_ok = unsafe {
EnumDisplaySettingsW(dd.device_name.as_ptr(), ENUM_CURRENT_SETTINGS, &mut dm)
};
let entry = if settings_ok != 0 && dm.pels_width > 0 && dm.pels_height > 0 {
if dm.display_frequency > 0 {
format!(
"{} ({}x{} @ {}Hz)",
adapter_name, dm.pels_width, dm.pels_height, dm.display_frequency
)
} else {
format!("{} ({}x{})", adapter_name, dm.pels_width, dm.pels_height)
}
} else {
adapter_name
};
if !entry.is_empty() {
displays.push(entry);
}
}
displays
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
let mut displays = Vec::new();
if let Ok(entries) = std::fs::read_dir("/sys/class/drm") {
for entry in entries.filter_map(|e| e.ok()) {
let path = entry.path();
let status_path = path.join("status");
let modes_path = path.join("modes");
let edid_path = path.join("edid");
if status_path.exists() && modes_path.exists() {
if let Ok(status) = std::fs::read_to_string(&status_path) {
if status.trim() == "connected" {
if let Ok(modes) = std::fs::read_to_string(&modes_path) {
if let Some(first_mode) = modes.lines().next() {
let res = first_mode.trim().to_string();
let port = entry.file_name().to_string_lossy().to_string();
let clean_port = if let Some(idx) = port.find('-') {
port[idx + 1..].to_string()
} else {
port
};
let edid_bytes = if edid_path.exists() {
std::fs::read(&edid_path).ok()
} else {
None
};
let name = edid_bytes
.as_ref()
.and_then(|bytes| parse_monitor_name_from_edid(bytes))
.unwrap_or(clean_port);
let refresh = edid_bytes
.as_ref()
.and_then(|bytes| parse_refresh_rate_from_edid(bytes));
let serial = edid_bytes
.as_ref()
.and_then(|bytes| parse_serial_number_from_edid(bytes));
let display_name = if let Some(ref s) = serial {
format!("{} #{}", name, s)
} else {
name
};
if let Some(r) = refresh {
displays.push(format!(
"{} ({} @ {}Hz)",
display_name,
res,
format_refresh_rate(r)
));
} else {
displays.push(format!("{} ({})", display_name, res));
}
}
}
}
}
}
}
}
if displays.is_empty() {
if let Ok(output) = std::process::Command::new("xrandr")
.arg("--current")
.output()
{
if let Ok(stdout) = String::from_utf8(output.stdout) {
displays = parse_xrandr_displays(&stdout);
}
}
}
displays
}
}
#[cfg(target_os = "macos")]
pub fn parse_macos_displays(stdout: &str) -> Vec<String> {
let mut displays = Vec::new();
let mut current_name = None;
let mut current_res = None;
let mut in_displays = false;
for line in stdout.lines() {
let trimmed = line.trim();
let indent = line.len() - line.trim_start().len();
if trimmed.starts_with("Displays:") {
in_displays = true;
continue;
}
if in_displays {
if indent < 8 && !trimmed.is_empty() && !trimmed.starts_with("Displays:") {
in_displays = false;
continue;
}
if trimmed.ends_with(':') && !trimmed.starts_with("UI Looks like:") {
let name = trimmed.trim_end_matches(':').trim().to_string();
current_name = Some(name);
} else if trimmed.starts_with("Resolution:") {
let res = trimmed.strip_prefix("Resolution:").unwrap_or("").trim();
let cleaned = res.replace(" ", "");
current_res = Some(cleaned);
} else if trimmed.starts_with("UI Looks like:") {
if let Some(res) = current_res.take() {
let name_str = current_name.take().unwrap_or_else(|| "Display".to_string());
if let Some(idx) = trimmed.find('@') {
let freq = trimmed[idx..].trim();
let freq_clean = freq.replace(" ", "").replace(".00", "");
displays.push(format!(
"{} ({} @ {})",
name_str,
res,
freq_clean.trim_start_matches('@')
));
} else {
displays.push(format!("{} ({})", name_str, res));
}
}
}
}
}
if let Some(res) = current_res {
let name_str = current_name.unwrap_or_else(|| "Display".to_string());
displays.push(format!("{} ({})", name_str, res));
}
displays
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
pub fn parse_xrandr_displays(stdout: &str) -> Vec<String> {
parse_xrandr_displays_with(stdout, get_monitor_name_for_port)
}
pub fn parse_xrandr_displays_with(
stdout: &str,
resolve: impl Fn(&str) -> Option<String>,
) -> Vec<String> {
let mut displays = Vec::new();
let mut current_display = None;
let mut current_port = None;
for line in stdout.lines() {
let line = line.trim();
if line.contains(" connected ") {
let parts: Vec<&str> = line.split_whitespace().collect();
if let Some(&port) = parts.first() {
current_port = Some(port.to_string());
}
for part in parts {
if part.contains('x') && part.contains('+') {
if let Some(res) = part.split('+').next() {
current_display = Some(res.to_string());
}
}
}
} else if line.contains('*') {
if let Some(res) = current_display.take() {
let port = current_port.take().unwrap_or_default();
let name = resolve(&port).unwrap_or_else(|| port.clone());
let parts: Vec<&str> = line.split_whitespace().collect();
let mut added = false;
for part in parts {
if part.contains('*') {
let freq = part.trim_end_matches(['*', '+']);
displays.push(format!("{} ({} @ {}Hz)", name, res, freq));
added = true;
break;
}
}
if !added {
displays.push(format!("{} ({})", name, res));
}
}
}
}
displays
}
#[cfg(test)]
mod tests {
use super::*;
fn edid_1080p60() -> Vec<u8> {
let mut edid = vec![0u8; 128];
edid[54] = 0x02;
edid[55] = 0x3A;
edid[56] = 0x80;
edid[57] = 0x18;
edid[58] = 0x71;
edid[59] = 0x38;
edid[60] = 0x2D;
edid[61] = 0x40;
edid
}
fn inject_monitor_name(edid: &mut Vec<u8>, name: &[u8]) {
edid[72] = 0x00;
edid[73] = 0x00;
edid[74] = 0x00;
edid[75] = 0xFC;
for (i, &b) in name.iter().enumerate().take(13) {
edid[76 + i] = b;
}
}
#[test]
fn test_monitor_name_too_short_edid() {
assert_eq!(parse_monitor_name_from_edid(&[0u8; 64]), None);
}
#[test]
fn test_monitor_name_no_descriptor() {
assert_eq!(parse_monitor_name_from_edid(&[0u8; 128]), None);
}
#[test]
fn test_monitor_name_at_offset_72() {
let mut edid = vec![0u8; 128];
inject_monitor_name(&mut edid, b"DELL S3422DW\n");
assert_eq!(
parse_monitor_name_from_edid(&edid),
Some("DELL S3422DW".to_string())
);
}
#[test]
fn test_monitor_name_at_offset_54() {
let mut edid = vec![0u8; 128];
edid[54] = 0x00;
edid[55] = 0x00;
edid[56] = 0x00;
edid[57] = 0xFC;
let name = b"LG 27UK850\n ";
for (i, &b) in name.iter().enumerate().take(13) {
edid[58 + i] = b;
}
assert_eq!(
parse_monitor_name_from_edid(&edid),
Some("LG 27UK850".to_string())
);
}
#[test]
fn test_parse_refresh_rate_from_edid() {
let edid = edid_1080p60();
let refresh = parse_refresh_rate_from_edid(&edid);
assert!(refresh.is_some());
assert_eq!(refresh.unwrap(), 60.0);
}
#[test]
fn test_refresh_rate_too_short_edid() {
assert_eq!(parse_refresh_rate_from_edid(&[0u8; 71]), None);
}
#[test]
fn test_refresh_rate_zero_pixel_clock() {
let edid = vec![0u8; 128];
assert_eq!(parse_refresh_rate_from_edid(&edid), None);
}
#[test]
fn test_refresh_rate_zero_totals() {
let mut edid = vec![0u8; 128];
edid[54] = 0x01; assert_eq!(parse_refresh_rate_from_edid(&edid), None);
}
#[test]
fn test_format_refresh_rate() {
assert_eq!(format_refresh_rate(60.0), "60");
assert_eq!(format_refresh_rate(59.94), "59.94");
assert_eq!(format_refresh_rate(143.971), "143.97");
assert_eq!(format_refresh_rate(120.0), "120");
assert_eq!(format_refresh_rate(240.0), "240");
}
#[test]
fn test_serial_too_short_edid() {
assert_eq!(parse_serial_number_from_edid(&[0u8; 64]), None);
}
#[test]
fn test_parse_serial_number_from_edid() {
let mut edid = vec![0u8; 128];
edid[12] = 0x78;
edid[13] = 0x56;
edid[14] = 0x34;
edid[15] = 0x12; assert_eq!(
parse_serial_number_from_edid(&edid),
Some("305419896".to_string())
);
edid[72] = 0x00;
edid[73] = 0x00;
edid[74] = 0x00;
edid[75] = 0xFF; let serial_str = b"CN0123456789\n";
for i in 0..serial_str.len() {
edid[76 + i] = serial_str[i];
}
assert_eq!(
parse_serial_number_from_edid(&edid),
Some("CN0123456789".to_string())
);
}
#[test]
fn test_serial_numeric_zero_is_none() {
let edid = vec![0u8; 128];
assert_eq!(parse_serial_number_from_edid(&edid), None);
}
#[test]
fn test_serial_numeric_all_ff_is_none() {
let mut edid = vec![0u8; 128];
edid[12] = 0xFF;
edid[13] = 0xFF;
edid[14] = 0xFF;
edid[15] = 0xFF;
assert_eq!(parse_serial_number_from_edid(&edid), None);
}
#[test]
fn test_serial_ascii_takes_precedence_over_numeric() {
let mut edid = vec![0u8; 128];
edid[12] = 0x01;
edid[13] = 0x02;
edid[14] = 0x03;
edid[15] = 0x04;
edid[54] = 0x00;
edid[55] = 0x00;
edid[56] = 0x00;
edid[57] = 0xFF;
let serial = b"ASCIIWIN0001\n";
for (i, &b) in serial.iter().enumerate().take(13) {
edid[58 + i] = b;
}
assert_eq!(
parse_serial_number_from_edid(&edid),
Some("ASCIIWIN0001".to_string())
);
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
#[test]
fn test_parse_xrandr_displays() {
let sample = "Screen 0: minimum 320 x 200, current 2560 x 1440\n\
DP-1 connected primary 2560x1440+0+0\n\
2560x1440 143.97*+\n\
1920x1080 60.00\n";
let parsed = parse_xrandr_displays_with(sample, |_| None);
assert_eq!(parsed, vec!["DP-1 (2560x1440 @ 143.97Hz)".to_string()]);
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
#[test]
fn test_parse_xrandr_multiple_displays() {
let sample = "Screen 0: minimum 320 x 200, current 3840 x 1080\n\
HDMI-1 connected 1920x1080+0+0\n\
1920x1080 60.00*+\n\
DP-1 connected 1920x1080+1920+0\n\
1920x1080 144.00*+\n";
let parsed = parse_xrandr_displays_with(sample, |_| None);
assert_eq!(
parsed,
vec![
"HDMI-1 (1920x1080 @ 60.00Hz)".to_string(),
"DP-1 (1920x1080 @ 144.00Hz)".to_string(),
]
);
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
#[test]
fn test_parse_xrandr_resolver_substitutes_monitor_name() {
let sample = "Screen 0: minimum 320 x 200, current 3840 x 1080\n\
HDMI-1 connected 1920x1080+0+0\n\
1920x1080 60.00*+\n\
DP-1 connected 1920x1080+1920+0\n\
1920x1080 144.00*+\n";
let parsed = parse_xrandr_displays_with(sample, |port| {
(port == "DP-1").then(|| "DELL S3422DW".to_string())
});
assert_eq!(
parsed,
vec![
"HDMI-1 (1920x1080 @ 60.00Hz)".to_string(),
"DELL S3422DW (1920x1080 @ 144.00Hz)".to_string(),
]
);
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
#[test]
fn test_parse_xrandr_no_connected_displays() {
let sample = "Screen 0: minimum 320 x 200, current 0 x 0\n\
HDMI-1 disconnected\n\
DP-1 disconnected\n";
assert_eq!(
parse_xrandr_displays_with(sample, |_| None),
Vec::<String>::new()
);
}
#[cfg(target_os = "macos")]
#[test]
fn test_parse_macos_displays() {
let sample = "Graphics/Displays:\n\n Apple M2:\n\n Chipset Model: Apple M2\n Displays:\n Color LCD:\n Resolution: 3024 x 1964\n UI Looks like: 1512 x 982 @ 60.00Hz\n";
let parsed = parse_macos_displays(sample);
assert_eq!(parsed, vec!["Color LCD (3024x1964 @ 60Hz)".to_string()]);
}
#[cfg(target_os = "macos")]
#[test]
fn test_parse_macos_no_frequency() {
let sample = "Graphics/Displays:\n Displays:\n ASUS PA329C:\n Resolution: 3840 x 2160\n";
let parsed = parse_macos_displays(sample);
assert_eq!(parsed, vec!["ASUS PA329C (3840x2160)".to_string()]);
}
}