1#[allow(dead_code)]
19pub fn parse_monitor_vendor_from_edid(edid: &[u8]) -> Option<&'static str> {
20 if edid.len() < 10 {
21 return None;
22 }
23 let val = ((edid[8] as u16) << 8) | (edid[9] as u16);
24 let c1 = (((val >> 10) & 0x1F) as u8 + b'@') as char;
25 let c2 = (((val >> 5) & 0x1F) as u8 + b'@') as char;
26 let c3 = ((val & 0x1F) as u8 + b'@') as char;
27
28 let pnp_id = format!("{}{}{}", c1, c2, c3);
29 match pnp_id.as_str() {
30 "SDC" | "SEC" => Some("Samsung"),
31 "GSM" | "LGD" | "LPL" => Some("LG"),
32 "DEL" => Some("Dell"),
33 "AUS" | "ACI" => Some("ASUS"),
34 "BEN" => Some("BenQ"),
35 "AOC" => Some("AOC"),
36 "ACR" => Some("Acer"),
37 "LEN" => Some("Lenovo"),
38 "HPN" | "HPQ" => Some("HP"),
39 "MSI" => Some("MSI"),
40 "SNY" => Some("Sony"),
41 "GBT" => Some("Gigabyte"),
42 "VSC" => Some("ViewSonic"),
43 "APP" => Some("Apple"),
44 "NEC" => Some("NEC"),
45 "PHL" => Some("Philips"),
46 _ => None,
47 }
48}
49
50#[allow(dead_code)]
57pub fn parse_monitor_name_from_edid(edid: &[u8]) -> Option<String> {
58 if edid.len() < 128 {
59 return None;
60 }
61 let vendor = parse_monitor_vendor_from_edid(edid);
62 let offsets = [54, 72, 90, 108];
63 for &offset in &offsets {
64 if offset + 18 <= edid.len() {
65 let block = &edid[offset..offset + 18];
66 if block[0] == 0x00 && block[1] == 0x00 && block[2] == 0x00 && block[3] == 0xFC {
67 let name_bytes = &block[4..17];
68 let name = String::from_utf8_lossy(name_bytes);
69 let cleaned = name.trim().replace('\0', "").to_string();
70 if !cleaned.is_empty() {
71 if let Some(v) = vendor {
72 if !cleaned.to_lowercase().starts_with(&v.to_lowercase()) {
73 return Some(format!("{} {}", v, cleaned));
74 }
75 }
76 return Some(cleaned);
77 }
78 }
79 }
80 }
81 None
82}
83
84#[allow(dead_code)]
93pub fn parse_refresh_rate_from_edid(edid: &[u8]) -> Option<f64> {
94 if edid.len() < 72 {
95 return None;
96 }
97 let block = &edid[54..72];
98 let pixel_clock = ((block[1] as u32) << 8) | (block[0] as u32);
99 if pixel_clock == 0 {
100 return None;
101 }
102 let pixel_clock_hz = pixel_clock * 10_000;
103 let h_active = (block[2] as u32) | (((block[4] as u32) & 0xF0) << 4);
104 let h_blanking = (block[3] as u32) | (((block[4] as u32) & 0x0F) << 8);
105 let v_active = (block[5] as u32) | (((block[7] as u32) & 0xF0) << 4);
106 let v_blanking = (block[6] as u32) | (((block[7] as u32) & 0x0F) << 8);
107
108 let h_total = h_active + h_blanking;
109 let v_total = v_active + v_blanking;
110 if h_total == 0 || v_total == 0 {
111 return None;
112 }
113
114 let refresh = (pixel_clock_hz as f64) / ((h_total * v_total) as f64);
115 Some((refresh * 100.0).round() / 100.0)
116}
117
118#[allow(dead_code)]
123pub fn format_refresh_rate(refresh: f64) -> String {
124 if (refresh - refresh.round()).abs() < 0.01 {
125 format!("{:.0}", refresh)
126 } else {
127 format!("{:.2}", refresh)
128 }
129}
130
131#[allow(dead_code)]
139pub fn parse_serial_number_from_edid(edid: &[u8]) -> Option<String> {
140 if edid.len() < 128 {
141 return None;
142 }
143 let offsets = [54, 72, 90, 108];
145 for &offset in &offsets {
146 if offset + 18 <= edid.len() {
147 let block = &edid[offset..offset + 18];
148 if block[0] == 0x00 && block[1] == 0x00 && block[2] == 0x00 && block[3] == 0xFF {
149 let serial_bytes = &block[4..17];
150 let serial = String::from_utf8_lossy(serial_bytes);
151 let cleaned = serial.trim().replace('\0', "").to_string();
152 if !cleaned.is_empty() {
153 return Some(cleaned);
154 }
155 }
156 }
157 }
158
159 let serial_num = ((edid[15] as u32) << 24)
161 | ((edid[14] as u32) << 16)
162 | ((edid[13] as u32) << 8)
163 | (edid[12] as u32);
164 if serial_num != 0 && serial_num != 0xFFFFFFFF {
165 return Some(serial_num.to_string());
166 }
167
168 None
169}
170
171#[allow(dead_code)]
178pub fn get_monitor_name_for_port(port: &str) -> Option<String> {
179 if let Ok(entries) = std::fs::read_dir("/sys/class/drm") {
180 for entry in entries.filter_map(|e| e.ok()) {
181 let name = entry.file_name().to_string_lossy().to_string();
182 if name.ends_with(port) {
183 let edid_path = entry.path().join("edid");
184 if edid_path.exists() {
185 if let Ok(edid_bytes) = std::fs::read(&edid_path) {
186 if let Some(monitor_name) = parse_monitor_name_from_edid(&edid_bytes) {
187 return Some(monitor_name);
188 }
189 }
190 }
191 }
192 }
193 }
194 None
195}
196
197pub fn detect_displays() -> Vec<String> {
205 #[cfg(target_os = "macos")]
206 {
207 crate::macos_ffi::get_displays()
208 }
209
210 #[cfg(target_os = "windows")]
211 {
212 #[repr(C)]
215 struct DisplayDevice {
216 cb: u32,
217 device_name: [u16; 32],
218 device_string: [u16; 128],
219 state_flags: u32,
220 device_id: [u16; 128],
221 device_key: [u16; 128],
222 }
223
224 #[repr(C)]
227 struct DevMode {
228 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")]
264 extern "system" {
265 fn EnumDisplayDevicesW(
266 lpDevice: *const u16,
267 iDevNum: u32,
268 lpDisplayDevice: *mut DisplayDevice,
269 dwFlags: u32,
270 ) -> i32;
271
272 fn EnumDisplaySettingsW(
273 lpszDeviceName: *const u16,
274 iModeNum: u32,
275 lpDevMode: *mut DevMode,
276 ) -> i32;
277 }
278
279 const DISPLAY_DEVICE_ACTIVE: u32 = 0x00000001;
280 const ENUM_CURRENT_SETTINGS: u32 = 0xFFFF_FFFF;
281
282 fn u16_to_string(buf: &[u16]) -> String {
283 let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
284 String::from_utf16_lossy(&buf[..len])
285 }
286
287 fn get_monitor_name(adapter_device_name: &[u16], fallback_adapter: &str) -> String {
288 let mut monitor_dd = DisplayDevice {
289 cb: std::mem::size_of::<DisplayDevice>() as u32,
290 device_name: [0u16; 32],
291 device_string: [0u16; 128],
292 state_flags: 0,
293 device_id: [0u16; 128],
294 device_key: [0u16; 128],
295 };
296
297 let ok =
298 unsafe { EnumDisplayDevicesW(adapter_device_name.as_ptr(), 0, &mut monitor_dd, 0) };
299
300 if ok != 0 {
301 let mon_string = u16_to_string(&monitor_dd.device_string);
302 let mon_id = u16_to_string(&monitor_dd.device_id);
303 let clean_id = mon_id
304 .trim_start_matches("\\\\?\\")
305 .trim_start_matches("\\\\.\\");
306
307 let parts: Vec<&str> = clean_id.split('\\').collect();
309 let mut candidate_hw_ids = Vec::new();
310 if parts.len() >= 2 {
311 candidate_hw_ids.push(parts[1].to_string());
312 }
313
314 for hw_id in candidate_hw_ids {
316 let dev_key = format!("SYSTEM\\CurrentControlSet\\Enum\\DISPLAY\\{}", hw_id);
317 let instances = crate::win_reg::enum_reg_subkeys(
318 crate::win_reg::HKEY_LOCAL_MACHINE,
319 &dev_key,
320 );
321 for inst in instances {
322 let subkey = format!(
323 "SYSTEM\\CurrentControlSet\\Enum\\DISPLAY\\{}\\{}\\Device Parameters",
324 hw_id, inst
325 );
326 if let Some(edid) = crate::win_reg::get_reg_bytes(
327 crate::win_reg::HKEY_LOCAL_MACHINE,
328 &subkey,
329 "EDID",
330 ) {
331 if let Some(edid_name) = parse_monitor_name_from_edid(&edid) {
332 return edid_name;
333 }
334 }
335 }
336 }
337
338 let display_devices = crate::win_reg::enum_reg_subkeys(
340 crate::win_reg::HKEY_LOCAL_MACHINE,
341 "SYSTEM\\CurrentControlSet\\Enum\\DISPLAY",
342 );
343 for dev in display_devices {
344 let instances = crate::win_reg::enum_reg_subkeys(
345 crate::win_reg::HKEY_LOCAL_MACHINE,
346 &format!("SYSTEM\\CurrentControlSet\\Enum\\DISPLAY\\{}", dev),
347 );
348 for inst in instances {
349 let subkey = format!(
350 "SYSTEM\\CurrentControlSet\\Enum\\DISPLAY\\{}\\{}\\Device Parameters",
351 dev, inst
352 );
353 if let Some(edid) = crate::win_reg::get_reg_bytes(
354 crate::win_reg::HKEY_LOCAL_MACHINE,
355 &subkey,
356 "EDID",
357 ) {
358 if let Some(edid_name) = parse_monitor_name_from_edid(&edid) {
359 return edid_name;
360 }
361 }
362 }
363 }
364
365 if !mon_string.is_empty()
366 && mon_string != "Generic PnP Monitor"
367 && mon_string != "Generic Monitor"
368 {
369 return mon_string;
370 }
371 }
372
373 fallback_adapter.to_string()
374 }
375
376 let mut displays = Vec::new();
377 let mut dev_num = 0u32;
378 loop {
379 let mut dd = DisplayDevice {
380 cb: std::mem::size_of::<DisplayDevice>() as u32,
381 device_name: [0u16; 32],
382 device_string: [0u16; 128],
383 state_flags: 0,
384 device_id: [0u16; 128],
385 device_key: [0u16; 128],
386 };
387 let ok = unsafe { EnumDisplayDevicesW(std::ptr::null(), dev_num, &mut dd, 0) };
388 if ok == 0 {
389 break;
390 }
391 dev_num += 1;
392
393 if dd.state_flags & DISPLAY_DEVICE_ACTIVE == 0 {
394 continue;
395 }
396
397 let adapter_name = u16_to_string(&dd.device_string);
398 let display_name = get_monitor_name(&dd.device_name, &adapter_name);
399
400 let mut dm = unsafe { std::mem::zeroed::<DevMode>() };
401 dm.size = std::mem::size_of::<DevMode>() as u16;
402 let settings_ok = unsafe {
403 EnumDisplaySettingsW(dd.device_name.as_ptr(), ENUM_CURRENT_SETTINGS, &mut dm)
404 };
405
406 let entry = if settings_ok != 0 && dm.pels_width > 0 && dm.pels_height > 0 {
407 if dm.display_frequency > 0 {
408 format!(
409 "{} ({}x{} @ {}Hz)",
410 display_name, dm.pels_width, dm.pels_height, dm.display_frequency
411 )
412 } else {
413 format!("{} ({}x{})", display_name, dm.pels_width, dm.pels_height)
414 }
415 } else {
416 display_name
417 };
418
419 if !entry.is_empty() {
420 displays.push(entry);
421 }
422 }
423
424 displays
425 }
426
427 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
428 {
429 let mut displays = Vec::new();
430
431 if let Ok(entries) = std::fs::read_dir("/sys/class/drm") {
433 for entry in entries.filter_map(|e| e.ok()) {
434 let path = entry.path();
435 let status_path = path.join("status");
436 let modes_path = path.join("modes");
437 let edid_path = path.join("edid");
438 if status_path.exists() && modes_path.exists() {
439 if let Ok(status) = std::fs::read_to_string(&status_path) {
440 if status.trim() == "connected" {
441 if let Ok(modes) = std::fs::read_to_string(&modes_path) {
442 if let Some(first_mode) = modes.lines().next() {
443 let res = first_mode.trim().to_string();
444 let port = entry.file_name().to_string_lossy().to_string();
445 let clean_port = if let Some(idx) = port.find('-') {
446 port[idx + 1..].to_string()
447 } else {
448 port
449 };
450
451 let edid_bytes = if edid_path.exists() {
452 std::fs::read(&edid_path).ok()
453 } else {
454 None
455 };
456
457 let name = edid_bytes
458 .as_ref()
459 .and_then(|bytes| parse_monitor_name_from_edid(bytes))
460 .unwrap_or(clean_port);
461
462 let refresh = edid_bytes
463 .as_ref()
464 .and_then(|bytes| parse_refresh_rate_from_edid(bytes));
465
466 let serial = edid_bytes
467 .as_ref()
468 .and_then(|bytes| parse_serial_number_from_edid(bytes));
469
470 let display_name = if let Some(ref s) = serial {
471 format!("{} #{}", name, s)
472 } else {
473 name
474 };
475
476 if let Some(r) = refresh {
477 displays.push(format!(
478 "{} ({} @ {}Hz)",
479 display_name,
480 res,
481 format_refresh_rate(r)
482 ));
483 } else {
484 displays.push(format!("{} ({})", display_name, res));
485 }
486 }
487 }
488 }
489 }
490 }
491 }
492 }
493
494 if displays.is_empty() {
496 if let Ok(output) = std::process::Command::new("xrandr")
497 .arg("--current")
498 .output()
499 {
500 if let Ok(stdout) = String::from_utf8(output.stdout) {
501 displays = parse_xrandr_displays(&stdout);
502 }
503 }
504 }
505
506 displays
507 }
508}
509
510#[cfg(target_os = "macos")]
514pub fn parse_macos_displays(stdout: &str) -> Vec<String> {
515 let mut displays = Vec::new();
516 let mut current_name = None;
517 let mut current_res = None;
518 let mut in_displays = false;
519
520 for line in stdout.lines() {
521 let trimmed = line.trim();
522 let indent = line.len() - line.trim_start().len();
523
524 if trimmed.starts_with("Displays:") {
525 in_displays = true;
526 continue;
527 }
528
529 if in_displays {
530 if indent < 8 && !trimmed.is_empty() && !trimmed.starts_with("Displays:") {
531 in_displays = false;
532 continue;
533 }
534
535 if trimmed.ends_with(':') && !trimmed.starts_with("UI Looks like:") {
536 let name = trimmed.trim_end_matches(':').trim().to_string();
537 current_name = Some(name);
538 } else if trimmed.starts_with("Resolution:") {
539 let res = trimmed.strip_prefix("Resolution:").unwrap_or("").trim();
540 let cleaned = res.replace(" ", "");
541 current_res = Some(cleaned);
542 } else if trimmed.starts_with("UI Looks like:") {
543 if let Some(res) = current_res.take() {
544 let name_str = current_name.take().unwrap_or_else(|| "Display".to_string());
545 if let Some(idx) = trimmed.find('@') {
546 let freq = trimmed[idx..].trim();
547 let freq_clean = freq.replace(" ", "").replace(".00", "");
548 displays.push(format!(
549 "{} ({} @ {})",
550 name_str,
551 res,
552 freq_clean.trim_start_matches('@')
553 ));
554 } else {
555 displays.push(format!("{} ({})", name_str, res));
556 }
557 }
558 }
559 }
560 }
561 if let Some(res) = current_res {
562 let name_str = current_name.unwrap_or_else(|| "Display".to_string());
563 displays.push(format!("{} ({})", name_str, res));
564 }
565 displays
566}
567
568#[cfg(not(any(target_os = "macos", target_os = "windows")))]
574pub fn parse_xrandr_displays(stdout: &str) -> Vec<String> {
575 parse_xrandr_displays_with(stdout, get_monitor_name_for_port)
576}
577
578pub fn parse_xrandr_displays_with(
588 stdout: &str,
589 resolve: impl Fn(&str) -> Option<String>,
590) -> Vec<String> {
591 let mut displays = Vec::new();
592 let mut current_display = None;
593 let mut current_port = None;
594 for line in stdout.lines() {
595 let line = line.trim();
596 if line.contains(" connected ") {
597 let parts: Vec<&str> = line.split_whitespace().collect();
598 if let Some(&port) = parts.first() {
599 current_port = Some(port.to_string());
600 }
601 for part in parts {
602 if part.contains('x') && part.contains('+') {
603 if let Some(res) = part.split('+').next() {
604 current_display = Some(res.to_string());
605 }
606 }
607 }
608 } else if line.contains('*') {
609 if let Some(res) = current_display.take() {
610 let port = current_port.take().unwrap_or_default();
611 let name = resolve(&port).unwrap_or_else(|| port.clone());
612 let parts: Vec<&str> = line.split_whitespace().collect();
613 let mut added = false;
614 for part in parts {
615 if part.contains('*') {
616 let freq = part.trim_end_matches(['*', '+']);
617 displays.push(format!("{} ({} @ {}Hz)", name, res, freq));
618 added = true;
619 break;
620 }
621 }
622 if !added {
623 displays.push(format!("{} ({})", name, res));
624 }
625 }
626 }
627 }
628 displays
629}
630
631#[cfg(test)]
632mod tests {
633 use super::*;
634
635 fn edid_1080p60() -> Vec<u8> {
639 let mut edid = vec![0u8; 128];
640 edid[54] = 0x02;
642 edid[55] = 0x3A;
643 edid[56] = 0x80;
645 edid[57] = 0x18;
646 edid[58] = 0x71;
647 edid[59] = 0x38;
649 edid[60] = 0x2D;
650 edid[61] = 0x40;
651 edid
652 }
653
654 fn inject_monitor_name(edid: &mut Vec<u8>, name: &[u8]) {
656 edid[72] = 0x00;
657 edid[73] = 0x00;
658 edid[74] = 0x00;
659 edid[75] = 0xFC;
660 for (i, &b) in name.iter().enumerate().take(13) {
661 edid[76 + i] = b;
662 }
663 }
664
665 #[test]
668 fn test_parse_monitor_vendor_from_edid() {
669 let mut edid = vec![0u8; 128];
670 edid[8] = 0x4C;
672 edid[9] = 0x83;
673 assert_eq!(parse_monitor_vendor_from_edid(&edid), Some("Samsung"));
674
675 edid[8] = 0x1E;
677 edid[9] = 0x6D;
678 assert_eq!(parse_monitor_vendor_from_edid(&edid), Some("LG"));
679 }
680
681 #[test]
682 fn test_monitor_name_prepends_vendor() {
683 let mut edid = vec![0u8; 128];
684 edid[8] = 0x4C;
685 edid[9] = 0x83; inject_monitor_name(&mut edid, b"ATNA33AA08-0");
687 assert_eq!(
688 parse_monitor_name_from_edid(&edid),
689 Some("Samsung ATNA33AA08-0".to_string())
690 );
691 }
692
693 #[test]
694 fn test_monitor_name_does_not_duplicate_vendor() {
695 let mut edid = vec![0u8; 128];
696 edid[8] = 0x1E;
697 edid[9] = 0x6D; inject_monitor_name(&mut edid, b"LG HDR 4K");
699 assert_eq!(
700 parse_monitor_name_from_edid(&edid),
701 Some("LG HDR 4K".to_string())
702 );
703 }
704
705 #[test]
706 fn test_monitor_name_too_short_edid() {
707 assert_eq!(parse_monitor_name_from_edid(&[0u8; 64]), None);
708 }
709
710 #[test]
711 fn test_monitor_name_no_descriptor() {
712 assert_eq!(parse_monitor_name_from_edid(&[0u8; 128]), None);
714 }
715
716 #[test]
717 fn test_monitor_name_at_offset_72() {
718 let mut edid = vec![0u8; 128];
719 inject_monitor_name(&mut edid, b"DELL S3422DW\n");
720 assert_eq!(
721 parse_monitor_name_from_edid(&edid),
722 Some("DELL S3422DW".to_string())
723 );
724 }
725
726 #[test]
727 fn test_monitor_name_at_offset_54() {
728 let mut edid = vec![0u8; 128];
729 edid[54] = 0x00;
730 edid[55] = 0x00;
731 edid[56] = 0x00;
732 edid[57] = 0xFC;
733 let name = b"LG 27UK850\n ";
734 for (i, &b) in name.iter().enumerate().take(13) {
735 edid[58 + i] = b;
736 }
737 assert_eq!(
738 parse_monitor_name_from_edid(&edid),
739 Some("LG 27UK850".to_string())
740 );
741 }
742
743 #[test]
746 fn test_parse_refresh_rate_from_edid() {
747 let edid = edid_1080p60();
748 let refresh = parse_refresh_rate_from_edid(&edid);
749 assert!(refresh.is_some());
750 assert_eq!(refresh.unwrap(), 60.0);
752 }
753
754 #[test]
755 fn test_refresh_rate_too_short_edid() {
756 assert_eq!(parse_refresh_rate_from_edid(&[0u8; 71]), None);
757 }
758
759 #[test]
760 fn test_refresh_rate_zero_pixel_clock() {
761 let edid = vec![0u8; 128];
763 assert_eq!(parse_refresh_rate_from_edid(&edid), None);
764 }
765
766 #[test]
767 fn test_refresh_rate_zero_totals() {
768 let mut edid = vec![0u8; 128];
770 edid[54] = 0x01; assert_eq!(parse_refresh_rate_from_edid(&edid), None);
773 }
774
775 #[test]
778 fn test_format_refresh_rate() {
779 assert_eq!(format_refresh_rate(60.0), "60");
780 assert_eq!(format_refresh_rate(59.94), "59.94");
781 assert_eq!(format_refresh_rate(143.971), "143.97");
782 assert_eq!(format_refresh_rate(120.0), "120");
783 assert_eq!(format_refresh_rate(240.0), "240");
784 }
785
786 #[test]
789 fn test_serial_too_short_edid() {
790 assert_eq!(parse_serial_number_from_edid(&[0u8; 64]), None);
791 }
792
793 #[test]
794 fn test_parse_serial_number_from_edid() {
795 let mut edid = vec![0u8; 128];
796 edid[12] = 0x78;
798 edid[13] = 0x56;
799 edid[14] = 0x34;
800 edid[15] = 0x12; assert_eq!(
802 parse_serial_number_from_edid(&edid),
803 Some("305419896".to_string())
804 );
805
806 edid[72] = 0x00;
808 edid[73] = 0x00;
809 edid[74] = 0x00;
810 edid[75] = 0xFF; let serial_str = b"CN0123456789\n";
812 for i in 0..serial_str.len() {
813 edid[76 + i] = serial_str[i];
814 }
815 assert_eq!(
816 parse_serial_number_from_edid(&edid),
817 Some("CN0123456789".to_string())
818 );
819 }
820
821 #[test]
822 fn test_serial_numeric_zero_is_none() {
823 let edid = vec![0u8; 128];
825 assert_eq!(parse_serial_number_from_edid(&edid), None);
826 }
827
828 #[test]
829 fn test_serial_numeric_all_ff_is_none() {
830 let mut edid = vec![0u8; 128];
831 edid[12] = 0xFF;
832 edid[13] = 0xFF;
833 edid[14] = 0xFF;
834 edid[15] = 0xFF;
835 assert_eq!(parse_serial_number_from_edid(&edid), None);
836 }
837
838 #[test]
839 fn test_serial_ascii_takes_precedence_over_numeric() {
840 let mut edid = vec![0u8; 128];
841 edid[12] = 0x01;
843 edid[13] = 0x02;
844 edid[14] = 0x03;
845 edid[15] = 0x04;
846 edid[54] = 0x00;
848 edid[55] = 0x00;
849 edid[56] = 0x00;
850 edid[57] = 0xFF;
851 let serial = b"ASCIIWIN0001\n";
852 for (i, &b) in serial.iter().enumerate().take(13) {
853 edid[58 + i] = b;
854 }
855 assert_eq!(
857 parse_serial_number_from_edid(&edid),
858 Some("ASCIIWIN0001".to_string())
859 );
860 }
861
862 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
865 #[test]
866 fn test_parse_xrandr_displays() {
867 let sample = "Screen 0: minimum 320 x 200, current 2560 x 1440\n\
868 DP-1 connected primary 2560x1440+0+0\n\
869 2560x1440 143.97*+\n\
870 1920x1080 60.00\n";
871 let parsed = parse_xrandr_displays_with(sample, |_| None);
872 assert_eq!(parsed, vec!["DP-1 (2560x1440 @ 143.97Hz)".to_string()]);
873 }
874
875 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
876 #[test]
877 fn test_parse_xrandr_multiple_displays() {
878 let sample = "Screen 0: minimum 320 x 200, current 3840 x 1080\n\
879 HDMI-1 connected 1920x1080+0+0\n\
880 1920x1080 60.00*+\n\
881 DP-1 connected 1920x1080+1920+0\n\
882 1920x1080 144.00*+\n";
883 let parsed = parse_xrandr_displays_with(sample, |_| None);
884 assert_eq!(
885 parsed,
886 vec![
887 "HDMI-1 (1920x1080 @ 60.00Hz)".to_string(),
888 "DP-1 (1920x1080 @ 144.00Hz)".to_string(),
889 ]
890 );
891 }
892
893 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
894 #[test]
895 fn test_parse_xrandr_resolver_substitutes_monitor_name() {
896 let sample = "Screen 0: minimum 320 x 200, current 3840 x 1080\n\
899 HDMI-1 connected 1920x1080+0+0\n\
900 1920x1080 60.00*+\n\
901 DP-1 connected 1920x1080+1920+0\n\
902 1920x1080 144.00*+\n";
903 let parsed = parse_xrandr_displays_with(sample, |port| {
904 (port == "DP-1").then(|| "DELL S3422DW".to_string())
905 });
906 assert_eq!(
907 parsed,
908 vec![
909 "HDMI-1 (1920x1080 @ 60.00Hz)".to_string(),
910 "DELL S3422DW (1920x1080 @ 144.00Hz)".to_string(),
911 ]
912 );
913 }
914
915 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
916 #[test]
917 fn test_parse_xrandr_no_connected_displays() {
918 let sample = "Screen 0: minimum 320 x 200, current 0 x 0\n\
919 HDMI-1 disconnected\n\
920 DP-1 disconnected\n";
921 assert_eq!(
922 parse_xrandr_displays_with(sample, |_| None),
923 Vec::<String>::new()
924 );
925 }
926
927 #[cfg(target_os = "macos")]
930 #[test]
931 fn test_parse_macos_displays() {
932 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";
933 let parsed = parse_macos_displays(sample);
934 assert_eq!(parsed, vec!["Color LCD (3024x1964 @ 60Hz)".to_string()]);
935 }
936
937 #[cfg(target_os = "windows")]
938 #[test]
939 fn test_windows_detect_displays_does_not_panic() {
940 let displays = detect_displays();
941 for d in displays {
942 assert!(!d.is_empty());
943 }
944 }
945}