1use std::sync::atomic::AtomicBool;
27use std::sync::Arc;
28
29pub fn spawn_monitor(active: Arc<AtomicBool>) {
30 #[cfg(target_os = "macos")]
31 macos::spawn(active);
32 #[cfg(target_os = "windows")]
33 windows::spawn(active);
34 #[cfg(target_os = "linux")]
35 linux::spawn(active);
36 #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
37 let _ = active;
38}
39
40const POLL_INTERVAL: std::time::Duration = std::time::Duration::from_secs(10);
46
47#[cfg_attr(target_os = "linux", allow(dead_code))]
60const FULLSCREEN_TOLERANCE_PX: i32 = 2;
61
62#[cfg_attr(target_os = "linux", allow(dead_code))]
65#[derive(Clone, Copy, Debug, PartialEq, Eq)]
66pub(crate) struct Rect {
67 pub x: i32,
68 pub y: i32,
69 pub w: i32,
70 pub h: i32,
71}
72
73#[cfg_attr(target_os = "linux", allow(dead_code))]
76pub(crate) fn rect_matches(window: Rect, monitor: Rect) -> bool {
77 (window.x - monitor.x).abs() <= FULLSCREEN_TOLERANCE_PX
78 && (window.y - monitor.y).abs() <= FULLSCREEN_TOLERANCE_PX
79 && (window.w - monitor.w).abs() <= FULLSCREEN_TOLERANCE_PX
80 && (window.h - monitor.h).abs() <= FULLSCREEN_TOLERANCE_PX
81}
82
83#[cfg_attr(target_os = "linux", allow(dead_code))]
86pub(crate) fn any_window_is_fullscreen(windows: &[Rect], monitors: &[Rect]) -> bool {
87 windows
88 .iter()
89 .any(|w| monitors.iter().any(|m| rect_matches(*w, *m)))
90}
91
92#[derive(Clone, Copy, Debug, PartialEq, Eq)]
98pub(crate) enum WindowKnowledge {
99 Fullscreen(bool),
100 #[cfg_attr(not(target_os = "linux"), allow(dead_code))]
104 Unknowable,
105}
106
107pub(crate) fn pause_decision(assertion_active: bool, window: WindowKnowledge) -> bool {
119 if !assertion_active {
120 return false;
121 }
122 match window {
123 WindowKnowledge::Fullscreen(b) => b,
124 WindowKnowledge::Unknowable => true,
125 }
126}
127
128#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
142pub(crate) fn parse_display_sleep_blocked(text: &str) -> bool {
143 for line in text.lines() {
144 let trimmed = line.trim_start();
145 let Some(rest) = trimmed.strip_prefix("PreventUserIdleDisplaySleep") else {
146 continue;
147 };
148 let count: u32 = rest.trim().parse().unwrap_or(0);
149 return count > 0;
150 }
151 false
152}
153
154#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
157pub(crate) fn parse_display_request(text: &str) -> bool {
158 let mut in_display = false;
159 for line in text.lines() {
160 let trimmed = line.trim();
161 if trimmed.eq_ignore_ascii_case("DISPLAY:") {
162 in_display = true;
163 continue;
164 }
165 if trimmed.ends_with(':') && !trimmed.eq_ignore_ascii_case("DISPLAY:") {
166 in_display = false;
167 continue;
168 }
169 if in_display && !trimmed.is_empty() && !trimmed.eq_ignore_ascii_case("None.") {
170 return true;
171 }
172 }
173 false
174}
175
176#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
180pub(crate) fn parse_idle_inhibitor(text: &str) -> bool {
181 for line in text.lines() {
182 for token in line.split_whitespace() {
183 if token.split(':').any(|w| w == "idle") {
184 return true;
185 }
186 }
187 }
188 false
189}
190
191#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
195pub(crate) fn parse_active_window_id(text: &str) -> Option<String> {
196 let (_, after) = text.trim().rsplit_once('#')?;
197 let id = after.trim();
198 if !id.starts_with("0x") || id == "0x0" {
199 return None;
200 }
201 Some(id.to_string())
202}
203
204#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
208pub(crate) fn parse_net_wm_state_fullscreen(text: &str) -> bool {
209 text.contains("_NET_WM_STATE_FULLSCREEN")
210}
211
212#[cfg(target_os = "macos")]
213mod macos {
214 use std::process::Command;
215 use std::sync::atomic::{AtomicBool, Ordering};
216 use std::sync::Arc;
217 use std::thread;
218
219 use crate::proc::CommandTimeoutExt;
220
221 use core_foundation::array::{CFArray, CFArrayRef};
222 use core_foundation::base::{TCFType, ToVoid};
223 use core_foundation::dictionary::CFDictionary;
224 use core_foundation::number::CFNumber;
225 use core_foundation::string::CFString;
226 use core_graphics::display::CGDisplay;
227 use core_graphics::window::{
228 kCGNullWindowID, kCGWindowListExcludeDesktopElements, kCGWindowListOptionOnScreenOnly,
229 CGWindowListCopyWindowInfo,
230 };
231
232 use super::{any_window_is_fullscreen, pause_decision, Rect, WindowKnowledge};
233
234 pub(super) const PMSET_BIN: &str = "/usr/bin/pmset";
238
239 pub fn spawn(active: Arc<AtomicBool>) {
240 thread::spawn(move || loop {
241 active.store(check(), Ordering::Relaxed);
242 thread::sleep(super::POLL_INTERVAL);
243 });
244 }
245
246 fn check() -> bool {
247 let assertion = display_assertion_active();
248 if !assertion {
251 return false;
252 }
253 pause_decision(
254 assertion,
255 WindowKnowledge::Fullscreen(fullscreen_window_present()),
256 )
257 }
258
259 pub(super) fn display_assertion_active() -> bool {
260 let Ok(output) = Command::new(PMSET_BIN)
261 .args(["-g", "assertions"])
262 .output_timeout(crate::proc::PROBE_TIMEOUT)
263 else {
264 return false;
265 };
266 if !output.status.success() {
267 return false;
268 }
269 let Ok(text) = std::str::from_utf8(&output.stdout) else {
270 return false;
271 };
272 super::parse_display_sleep_blocked(text)
273 }
274
275 fn fullscreen_window_present() -> bool {
276 let Some(monitors) = active_display_bounds() else {
277 return false;
278 };
279 let windows = onscreen_app_window_bounds();
280 any_window_is_fullscreen(&windows, &monitors)
281 }
282
283 fn active_display_bounds() -> Option<Vec<Rect>> {
284 let ids = CGDisplay::active_displays().ok()?;
285 let mut out = Vec::with_capacity(ids.len());
286 for id in ids {
287 let b = CGDisplay::new(id).bounds();
288 out.push(Rect {
289 x: b.origin.x as i32,
290 y: b.origin.y as i32,
291 w: b.size.width as i32,
292 h: b.size.height as i32,
293 });
294 }
295 Some(out)
296 }
297
298 fn onscreen_app_window_bounds() -> Vec<Rect> {
299 let array_ref: CFArrayRef = unsafe {
303 CGWindowListCopyWindowInfo(
304 kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements,
305 kCGNullWindowID,
306 )
307 };
308 if array_ref.is_null() {
309 return Vec::new();
310 }
311 let array: CFArray<CFDictionary> = unsafe { CFArray::wrap_under_create_rule(array_ref) };
312
313 let mut out = Vec::new();
314 for dict in array.iter() {
315 let dict: &CFDictionary = &dict;
316 if !is_normal_app_window(dict) {
317 continue;
318 }
319 if let Some(rect) = window_bounds(dict) {
320 out.push(rect);
321 }
322 }
323 out
324 }
325
326 fn is_normal_app_window(dict: &CFDictionary) -> bool {
327 let key = CFString::from_static_string("kCGWindowLayer");
332 let raw = match dict.find(key.to_void()) {
333 Some(v) => v,
334 None => return false,
335 };
336 let num = unsafe { CFNumber::wrap_under_get_rule(*raw as _) };
337 num.to_i32() == Some(0)
338 }
339
340 fn window_bounds(dict: &CFDictionary) -> Option<Rect> {
341 let key = CFString::from_static_string("kCGWindowBounds");
342 let raw = dict.find(key.to_void())?;
343 let bounds_dict: CFDictionary = unsafe { CFDictionary::wrap_under_get_rule(*raw as _) };
344 Some(Rect {
345 x: dict_f64(&bounds_dict, "X")? as i32,
346 y: dict_f64(&bounds_dict, "Y")? as i32,
347 w: dict_f64(&bounds_dict, "Width")? as i32,
348 h: dict_f64(&bounds_dict, "Height")? as i32,
349 })
350 }
351
352 fn dict_f64(dict: &CFDictionary, key: &str) -> Option<f64> {
353 let key = CFString::new(key);
354 let raw = dict.find(key.to_void())?;
355 let num = unsafe { CFNumber::wrap_under_get_rule(*raw as _) };
356 num.to_f64()
357 }
358}
359
360#[cfg(target_os = "windows")]
361mod windows {
362 use std::process::Command;
363 use std::sync::atomic::{AtomicBool, Ordering};
364
365 use crate::proc::CommandTimeoutExt;
366 use std::sync::Arc;
367 use std::thread;
368
369 use windows_sys::Win32::Foundation::{BOOL, HWND, LPARAM, RECT, TRUE};
370 use windows_sys::Win32::Graphics::Gdi::{
371 EnumDisplayMonitors, GetMonitorInfoW, HDC, HMONITOR, MONITORINFO,
372 };
373 use windows_sys::Win32::UI::WindowsAndMessaging::{
374 EnumWindows, GetWindowRect, IsWindowVisible,
375 };
376
377 use super::{any_window_is_fullscreen, pause_decision, Rect, WindowKnowledge};
378
379 pub(super) const POWERCFG_BIN: &str = r"C:\Windows\System32\powercfg.exe";
383
384 pub fn spawn(active: Arc<AtomicBool>) {
385 thread::spawn(move || loop {
386 active.store(check(), Ordering::Relaxed);
387 thread::sleep(super::POLL_INTERVAL);
388 });
389 }
390
391 fn check() -> bool {
392 let assertion = display_request_active();
393 if !assertion {
394 return false;
395 }
396 pause_decision(
397 assertion,
398 WindowKnowledge::Fullscreen(fullscreen_window_present()),
399 )
400 }
401
402 pub(super) fn display_request_active() -> bool {
403 let Ok(output) = Command::new(POWERCFG_BIN)
404 .arg("/requests")
405 .output_timeout(crate::proc::PROBE_TIMEOUT)
406 else {
407 return false;
408 };
409 if !output.status.success() {
410 return false;
411 }
412 let Ok(text) = std::str::from_utf8(&output.stdout) else {
413 return false;
414 };
415 super::parse_display_request(text)
416 }
417
418 fn fullscreen_window_present() -> bool {
419 let monitors = enumerate_monitors();
420 if monitors.is_empty() {
421 return false;
422 }
423 let windows = enumerate_visible_windows();
424 any_window_is_fullscreen(&windows, &monitors)
425 }
426
427 fn enumerate_monitors() -> Vec<Rect> {
428 let mut out: Vec<Rect> = Vec::new();
429 let ptr: *mut Vec<Rect> = &mut out;
430 unsafe {
433 EnumDisplayMonitors(
434 std::ptr::null_mut(),
435 std::ptr::null(),
436 Some(monitor_enum_proc),
437 ptr as isize,
438 );
439 }
440 out
441 }
442
443 unsafe extern "system" fn monitor_enum_proc(
444 hmon: HMONITOR,
445 _hdc: HDC,
446 _rect: *mut RECT,
447 lparam: LPARAM,
448 ) -> BOOL {
449 let mut info: MONITORINFO = std::mem::zeroed();
450 info.cbSize = std::mem::size_of::<MONITORINFO>() as u32;
451 if GetMonitorInfoW(hmon, &mut info) != 0 {
452 let r = info.rcMonitor;
453 let out = &mut *(lparam as *mut Vec<Rect>);
454 out.push(Rect {
455 x: r.left,
456 y: r.top,
457 w: r.right - r.left,
458 h: r.bottom - r.top,
459 });
460 }
461 TRUE
462 }
463
464 fn enumerate_visible_windows() -> Vec<Rect> {
465 let mut out: Vec<Rect> = Vec::new();
466 let ptr: *mut Vec<Rect> = &mut out;
467 unsafe {
470 EnumWindows(Some(enum_windows_proc), ptr as isize);
471 }
472 out
473 }
474
475 unsafe extern "system" fn enum_windows_proc(hwnd: HWND, lparam: LPARAM) -> BOOL {
476 if IsWindowVisible(hwnd) == 0 {
477 return TRUE;
478 }
479 let mut r: RECT = std::mem::zeroed();
480 if GetWindowRect(hwnd, &mut r) == 0 {
481 return TRUE;
482 }
483 let out = &mut *(lparam as *mut Vec<Rect>);
484 out.push(Rect {
485 x: r.left,
486 y: r.top,
487 w: r.right - r.left,
488 h: r.bottom - r.top,
489 });
490 TRUE
491 }
492}
493
494#[cfg(target_os = "linux")]
495mod linux {
496 use std::env;
497 use std::process::Command;
498
499 use crate::proc::CommandTimeoutExt;
500 use std::sync::atomic::{AtomicBool, Ordering};
501 use std::sync::Arc;
502 use std::sync::OnceLock;
503 use std::thread;
504
505 use super::{pause_decision, WindowKnowledge};
506
507 pub(super) const SYSTEMD_INHIBIT_BIN: &str = "/usr/bin/systemd-inhibit";
511
512 pub(super) const XPROP_BIN: &str = "/usr/bin/xprop";
517
518 pub fn spawn(active: Arc<AtomicBool>) {
519 log_wayland_degradation_once();
520 thread::spawn(move || loop {
521 active.store(check(), Ordering::Relaxed);
522 thread::sleep(super::POLL_INTERVAL);
523 });
524 }
525
526 fn check() -> bool {
527 let assertion = inhibitor_active();
528 if !assertion {
529 return false;
530 }
531 let window = if is_wayland_session() {
535 WindowKnowledge::Unknowable
536 } else {
537 WindowKnowledge::Fullscreen(fullscreen_window_present())
538 };
539 pause_decision(assertion, window)
540 }
541
542 fn inhibitor_active() -> bool {
543 let Ok(output) = Command::new(SYSTEMD_INHIBIT_BIN)
544 .args(["--list", "--no-pager", "--no-legend"])
545 .output_timeout(crate::proc::PROBE_TIMEOUT)
546 else {
547 return false;
548 };
549 if !output.status.success() {
550 return false;
551 }
552 let Ok(text) = std::str::from_utf8(&output.stdout) else {
553 return false;
554 };
555 super::parse_idle_inhibitor(text)
556 }
557
558 fn is_wayland_session() -> bool {
559 env::var("XDG_SESSION_TYPE")
560 .map(|s| s.eq_ignore_ascii_case("wayland"))
561 .unwrap_or(false)
562 || env::var("WAYLAND_DISPLAY").is_ok()
563 }
564
565 fn log_wayland_degradation_once() {
566 static LOGGED: OnceLock<()> = OnceLock::new();
567 if is_wayland_session() {
568 LOGGED.get_or_init(|| {
569 log::info!(
570 "video: Wayland detected — falling back to assertion-only \
571 (no portable way to enumerate fullscreen windows on Wayland)"
572 );
573 });
574 }
575 }
576
577 fn fullscreen_window_present() -> bool {
578 let Some(active_id) = xprop_active_window_id() else {
579 return false;
580 };
581 let Some(state) = xprop_window_state(&active_id) else {
582 return false;
583 };
584 super::parse_net_wm_state_fullscreen(&state)
585 }
586
587 fn xprop_active_window_id() -> Option<String> {
588 let out = Command::new(XPROP_BIN)
589 .args(["-root", "_NET_ACTIVE_WINDOW"])
590 .output_timeout(crate::proc::PROBE_TIMEOUT)
591 .ok()?;
592 if !out.status.success() {
593 return None;
594 }
595 let text = std::str::from_utf8(&out.stdout).ok()?;
596 super::parse_active_window_id(text)
597 }
598
599 fn xprop_window_state(id: &str) -> Option<String> {
600 let out = Command::new(XPROP_BIN)
601 .args(["-id", id, "_NET_WM_STATE"])
602 .output_timeout(crate::proc::PROBE_TIMEOUT)
603 .ok()?;
604 if !out.status.success() {
605 return None;
606 }
607 std::str::from_utf8(&out.stdout).ok().map(str::to_string)
608 }
609}
610
611#[cfg(test)]
612mod tests {
613 use super::*;
614
615 #[test]
616 fn rect_matches_exact() {
617 let r = Rect {
618 x: 0,
619 y: 0,
620 w: 1920,
621 h: 1080,
622 };
623 assert!(rect_matches(r, r));
624 }
625
626 #[test]
627 fn rect_matches_within_tolerance() {
628 let win = Rect {
629 x: 1,
630 y: 0,
631 w: 1919,
632 h: 1081,
633 };
634 let mon = Rect {
635 x: 0,
636 y: 0,
637 w: 1920,
638 h: 1080,
639 };
640 assert!(rect_matches(win, mon));
641 }
642
643 #[test]
644 fn rect_does_not_match_outside_tolerance() {
645 let win = Rect {
646 x: 0,
647 y: 0,
648 w: 1910,
649 h: 1080,
650 };
651 let mon = Rect {
652 x: 0,
653 y: 0,
654 w: 1920,
655 h: 1080,
656 };
657 assert!(!rect_matches(win, mon));
658 }
659
660 #[test]
661 fn any_window_is_fullscreen_finds_match_across_multiple_monitors() {
662 let windows = [
663 Rect {
664 x: 0,
665 y: 0,
666 w: 800,
667 h: 600,
668 }, Rect {
670 x: 1920,
671 y: 0,
672 w: 2560,
673 h: 1440,
674 }, ];
676 let monitors = [
677 Rect {
678 x: 0,
679 y: 0,
680 w: 1920,
681 h: 1080,
682 },
683 Rect {
684 x: 1920,
685 y: 0,
686 w: 2560,
687 h: 1440,
688 },
689 ];
690 assert!(any_window_is_fullscreen(&windows, &monitors));
691 }
692
693 #[test]
694 fn any_window_is_fullscreen_false_when_nothing_matches() {
695 let windows = [
696 Rect {
697 x: 100,
698 y: 100,
699 w: 800,
700 h: 600,
701 },
702 Rect {
703 x: 0,
704 y: 0,
705 w: 1280,
706 h: 720,
707 },
708 ];
709 let monitors = [Rect {
710 x: 0,
711 y: 0,
712 w: 1920,
713 h: 1080,
714 }];
715 assert!(!any_window_is_fullscreen(&windows, &monitors));
716 }
717
718 #[test]
719 fn any_window_is_fullscreen_false_for_maximised_but_not_fullscreen() {
720 let windows = [Rect {
723 x: 0,
724 y: 0,
725 w: 1920,
726 h: 1040,
727 }];
728 let monitors = [Rect {
729 x: 0,
730 y: 0,
731 w: 1920,
732 h: 1080,
733 }];
734 assert!(!any_window_is_fullscreen(&windows, &monitors));
735 }
736
737 #[test]
743 fn pause_decision_no_pause_without_assertion() {
744 assert!(!pause_decision(false, WindowKnowledge::Fullscreen(true)));
745 assert!(!pause_decision(false, WindowKnowledge::Fullscreen(false)));
746 assert!(!pause_decision(false, WindowKnowledge::Unknowable));
747 }
748
749 #[test]
750 fn pause_decision_pauses_when_assertion_and_fullscreen() {
751 assert!(pause_decision(true, WindowKnowledge::Fullscreen(true)));
752 }
753
754 #[test]
755 fn pause_decision_does_not_pause_for_small_window_video() {
756 assert!(!pause_decision(true, WindowKnowledge::Fullscreen(false)));
759 }
760
761 #[test]
762 fn pause_decision_falls_back_to_assertion_only_when_window_unknowable() {
763 assert!(pause_decision(true, WindowKnowledge::Unknowable));
766 }
767
768 #[test]
771 fn pmset_no_assertions_means_inactive() {
772 let sample = "Assertion status system-wide:\n PreventUserIdleDisplaySleep 0\n UserIsActive 1\n";
773 assert!(!parse_display_sleep_blocked(sample));
774 }
775
776 #[test]
777 fn pmset_nonzero_count_means_active() {
778 let sample = "Assertion status system-wide:\n PreventUserIdleDisplaySleep 1\n UserIsActive 1\n";
779 assert!(parse_display_sleep_blocked(sample));
780 }
781
782 #[test]
783 fn pmset_higher_counts_still_active() {
784 let sample = " PreventUserIdleDisplaySleep 3\n";
785 assert!(parse_display_sleep_blocked(sample));
786 }
787
788 #[test]
789 fn pmset_missing_key_means_inactive() {
790 let sample = "Assertion status system-wide:\n UserIsActive 1\n";
791 assert!(!parse_display_sleep_blocked(sample));
792 }
793
794 #[test]
795 fn pmset_garbled_count_means_inactive() {
796 let sample = " PreventUserIdleDisplaySleep NaN\n";
797 assert!(!parse_display_sleep_blocked(sample));
798 }
799
800 #[test]
803 fn powercfg_all_none_means_inactive() {
804 let sample = "DISPLAY:\nNone.\n\nSYSTEM:\nNone.\n\nAWAYMODE:\nNone.\n";
805 assert!(!parse_display_request(sample));
806 }
807
808 #[test]
809 fn powercfg_display_process_means_active() {
810 let sample =
811 "DISPLAY:\n[PROCESS] \\Device\\HarddiskVolume3\\firefox.exe\n\nSYSTEM:\nNone.\n";
812 assert!(parse_display_request(sample));
813 }
814
815 #[test]
816 fn powercfg_only_system_request_is_inactive() {
817 let sample = "DISPLAY:\nNone.\n\nSYSTEM:\n[DRIVER] Realtek HD Audio\n";
818 assert!(!parse_display_request(sample));
819 }
820
821 #[test]
824 fn inhibit_empty_means_inactive() {
825 assert!(!parse_idle_inhibitor(""));
826 }
827
828 #[test]
829 fn inhibit_idle_what_means_active() {
830 let sample = "user 1000 alice 12345 firefox idle Playing:video block\n";
831 assert!(parse_idle_inhibitor(sample));
832 }
833
834 #[test]
835 fn inhibit_compound_what_with_idle_means_active() {
836 let sample = "user 1000 alice 12345 vlc sleep:idle Playing block\n";
837 assert!(parse_idle_inhibitor(sample));
838 }
839
840 #[test]
841 fn inhibit_non_idle_inhibitor_is_inactive() {
842 let sample = "user 1000 alice 12345 systemd-logind handle-power-key:handle-suspend-key Lid closed block\n";
843 assert!(!parse_idle_inhibitor(sample));
844 }
845
846 #[test]
847 fn inhibit_substring_idle_does_not_match() {
848 let sample = "user 1000 alice 12345 daemon sleep Process-is-idle-checker block\n";
849 assert!(!parse_idle_inhibitor(sample));
850 }
851
852 #[test]
855 fn active_window_id_extracts_hex_id() {
856 let sample = "_NET_ACTIVE_WINDOW(WINDOW): window id # 0x3c00006\n";
857 assert_eq!(
858 parse_active_window_id(sample),
859 Some("0x3c00006".to_string())
860 );
861 }
862
863 #[test]
864 fn active_window_id_rejects_non_hex() {
865 let sample = "_NET_ACTIVE_WINDOW(WINDOW): not set\n";
866 assert_eq!(parse_active_window_id(sample), None);
867 }
868
869 #[test]
870 fn active_window_id_rejects_zero_sentinel() {
871 let sample = "_NET_ACTIVE_WINDOW(WINDOW): window id # 0x0\n";
875 assert_eq!(parse_active_window_id(sample), None);
876 }
877
878 #[test]
881 fn net_wm_state_true_when_fullscreen_present() {
882 let sample = "_NET_WM_STATE(ATOM) = _NET_WM_STATE_FULLSCREEN\n";
883 assert!(parse_net_wm_state_fullscreen(sample));
884 }
885
886 #[test]
887 fn net_wm_state_true_when_fullscreen_combined_with_other_states() {
888 let sample = "_NET_WM_STATE(ATOM) = _NET_WM_STATE_FULLSCREEN, _NET_WM_STATE_ABOVE\n";
890 assert!(parse_net_wm_state_fullscreen(sample));
891 }
892
893 #[test]
894 fn net_wm_state_false_for_maximised() {
895 let sample =
899 "_NET_WM_STATE(ATOM) = _NET_WM_STATE_MAXIMIZED_HORZ, _NET_WM_STATE_MAXIMIZED_VERT\n";
900 assert!(!parse_net_wm_state_fullscreen(sample));
901 }
902
903 #[test]
904 fn net_wm_state_false_when_property_missing() {
905 let sample = "_NET_WM_STATE: not found.\n";
907 assert!(!parse_net_wm_state_fullscreen(sample));
908 }
909
910 #[test]
911 fn net_wm_state_false_for_empty_output() {
912 assert!(!parse_net_wm_state_fullscreen(""));
913 }
914}
915
916#[cfg(all(test, target_os = "macos"))]
919mod macos_bin_tests {
920 use super::macos::PMSET_BIN;
921
922 #[test]
923 fn pmset_bin_is_absolute_and_non_empty() {
924 assert!(!PMSET_BIN.is_empty());
925 assert!(
926 PMSET_BIN.starts_with('/'),
927 "expected absolute path, got {PMSET_BIN}"
928 );
929 }
930}
931
932#[cfg(all(test, target_os = "windows"))]
933mod windows_bin_tests {
934 use super::windows::POWERCFG_BIN;
935
936 #[test]
937 fn powercfg_bin_is_absolute_and_non_empty() {
938 assert!(!POWERCFG_BIN.is_empty());
939 assert!(
942 POWERCFG_BIN.contains(":\\"),
943 "expected absolute Windows path, got {POWERCFG_BIN}"
944 );
945 }
946}
947
948#[cfg(all(test, target_os = "linux"))]
949mod linux_bin_tests {
950 use super::linux::SYSTEMD_INHIBIT_BIN;
951
952 #[test]
953 fn systemd_inhibit_bin_is_absolute_and_non_empty() {
954 assert!(!SYSTEMD_INHIBIT_BIN.is_empty());
955 assert!(
956 SYSTEMD_INHIBIT_BIN.starts_with('/'),
957 "expected absolute path, got {SYSTEMD_INHIBIT_BIN}"
958 );
959 }
960}