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(target_os = "macos")]
129mod macos {
130 use std::process::Command;
131 use std::sync::atomic::{AtomicBool, Ordering};
132 use std::sync::Arc;
133 use std::thread;
134
135 use core_foundation::array::{CFArray, CFArrayRef};
136 use core_foundation::base::{TCFType, ToVoid};
137 use core_foundation::dictionary::CFDictionary;
138 use core_foundation::number::CFNumber;
139 use core_foundation::string::CFString;
140 use core_graphics::display::CGDisplay;
141 use core_graphics::window::{
142 kCGNullWindowID, kCGWindowListExcludeDesktopElements, kCGWindowListOptionOnScreenOnly,
143 CGWindowListCopyWindowInfo,
144 };
145
146 use super::{any_window_is_fullscreen, pause_decision, Rect, WindowKnowledge};
147
148 pub(super) const PMSET_BIN: &str = "/usr/bin/pmset";
152
153 pub fn spawn(active: Arc<AtomicBool>) {
154 thread::spawn(move || loop {
155 active.store(check(), Ordering::Relaxed);
156 thread::sleep(super::POLL_INTERVAL);
157 });
158 }
159
160 fn check() -> bool {
161 let assertion = display_assertion_active();
162 if !assertion {
165 return false;
166 }
167 pause_decision(
168 assertion,
169 WindowKnowledge::Fullscreen(fullscreen_window_present()),
170 )
171 }
172
173 fn display_assertion_active() -> bool {
174 let Ok(output) = Command::new(PMSET_BIN).args(["-g", "assertions"]).output() else {
175 return false;
176 };
177 if !output.status.success() {
178 return false;
179 }
180 let Ok(text) = std::str::from_utf8(&output.stdout) else {
181 return false;
182 };
183 parse_display_sleep_blocked(text)
184 }
185
186 pub(super) fn parse_display_sleep_blocked(text: &str) -> bool {
187 for line in text.lines() {
188 let trimmed = line.trim_start();
189 let Some(rest) = trimmed.strip_prefix("PreventUserIdleDisplaySleep") else {
190 continue;
191 };
192 let count: u32 = rest.trim().parse().unwrap_or(0);
193 return count > 0;
194 }
195 false
196 }
197
198 fn fullscreen_window_present() -> bool {
199 let Some(monitors) = active_display_bounds() else {
200 return false;
201 };
202 let windows = onscreen_app_window_bounds();
203 any_window_is_fullscreen(&windows, &monitors)
204 }
205
206 fn active_display_bounds() -> Option<Vec<Rect>> {
207 let ids = CGDisplay::active_displays().ok()?;
208 let mut out = Vec::with_capacity(ids.len());
209 for id in ids {
210 let b = CGDisplay::new(id).bounds();
211 out.push(Rect {
212 x: b.origin.x as i32,
213 y: b.origin.y as i32,
214 w: b.size.width as i32,
215 h: b.size.height as i32,
216 });
217 }
218 Some(out)
219 }
220
221 fn onscreen_app_window_bounds() -> Vec<Rect> {
222 let array_ref: CFArrayRef = unsafe {
226 CGWindowListCopyWindowInfo(
227 kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements,
228 kCGNullWindowID,
229 )
230 };
231 if array_ref.is_null() {
232 return Vec::new();
233 }
234 let array: CFArray<CFDictionary> = unsafe { CFArray::wrap_under_create_rule(array_ref) };
235
236 let mut out = Vec::new();
237 for dict in array.iter() {
238 let dict: &CFDictionary = &dict;
239 if !is_normal_app_window(dict) {
240 continue;
241 }
242 if let Some(rect) = window_bounds(dict) {
243 out.push(rect);
244 }
245 }
246 out
247 }
248
249 fn is_normal_app_window(dict: &CFDictionary) -> bool {
250 let key = CFString::from_static_string("kCGWindowLayer");
255 let raw = match dict.find(key.to_void()) {
256 Some(v) => v,
257 None => return false,
258 };
259 let num = unsafe { CFNumber::wrap_under_get_rule(*raw as _) };
260 num.to_i32() == Some(0)
261 }
262
263 fn window_bounds(dict: &CFDictionary) -> Option<Rect> {
264 let key = CFString::from_static_string("kCGWindowBounds");
265 let raw = dict.find(key.to_void())?;
266 let bounds_dict: CFDictionary = unsafe { CFDictionary::wrap_under_get_rule(*raw as _) };
267 Some(Rect {
268 x: dict_f64(&bounds_dict, "X")? as i32,
269 y: dict_f64(&bounds_dict, "Y")? as i32,
270 w: dict_f64(&bounds_dict, "Width")? as i32,
271 h: dict_f64(&bounds_dict, "Height")? as i32,
272 })
273 }
274
275 fn dict_f64(dict: &CFDictionary, key: &str) -> Option<f64> {
276 let key = CFString::new(key);
277 let raw = dict.find(key.to_void())?;
278 let num = unsafe { CFNumber::wrap_under_get_rule(*raw as _) };
279 num.to_f64()
280 }
281}
282
283#[cfg(target_os = "windows")]
284mod windows {
285 use std::process::Command;
286 use std::sync::atomic::{AtomicBool, Ordering};
287 use std::sync::Arc;
288 use std::thread;
289
290 use windows_sys::Win32::Foundation::{BOOL, HWND, LPARAM, RECT, TRUE};
291 use windows_sys::Win32::Graphics::Gdi::{
292 EnumDisplayMonitors, GetMonitorInfoW, HDC, HMONITOR, MONITORINFO,
293 };
294 use windows_sys::Win32::UI::WindowsAndMessaging::{
295 EnumWindows, GetWindowRect, IsWindowVisible,
296 };
297
298 use super::{any_window_is_fullscreen, pause_decision, Rect, WindowKnowledge};
299
300 pub(super) const POWERCFG_BIN: &str = r"C:\Windows\System32\powercfg.exe";
304
305 pub fn spawn(active: Arc<AtomicBool>) {
306 thread::spawn(move || loop {
307 active.store(check(), Ordering::Relaxed);
308 thread::sleep(super::POLL_INTERVAL);
309 });
310 }
311
312 fn check() -> bool {
313 let assertion = display_request_active();
314 if !assertion {
315 return false;
316 }
317 pause_decision(
318 assertion,
319 WindowKnowledge::Fullscreen(fullscreen_window_present()),
320 )
321 }
322
323 fn display_request_active() -> bool {
324 let Ok(output) = Command::new(POWERCFG_BIN).arg("/requests").output() else {
325 return false;
326 };
327 if !output.status.success() {
328 return false;
329 }
330 let Ok(text) = std::str::from_utf8(&output.stdout) else {
331 return false;
332 };
333 parse_display_request(text)
334 }
335
336 pub(super) fn parse_display_request(text: &str) -> bool {
337 let mut in_display = false;
338 for line in text.lines() {
339 let trimmed = line.trim();
340 if trimmed.eq_ignore_ascii_case("DISPLAY:") {
341 in_display = true;
342 continue;
343 }
344 if trimmed.ends_with(':') && !trimmed.eq_ignore_ascii_case("DISPLAY:") {
345 in_display = false;
346 continue;
347 }
348 if in_display && !trimmed.is_empty() && !trimmed.eq_ignore_ascii_case("None.") {
349 return true;
350 }
351 }
352 false
353 }
354
355 fn fullscreen_window_present() -> bool {
356 let monitors = enumerate_monitors();
357 if monitors.is_empty() {
358 return false;
359 }
360 let windows = enumerate_visible_windows();
361 any_window_is_fullscreen(&windows, &monitors)
362 }
363
364 fn enumerate_monitors() -> Vec<Rect> {
365 let mut out: Vec<Rect> = Vec::new();
366 let ptr: *mut Vec<Rect> = &mut out;
367 unsafe {
370 EnumDisplayMonitors(
371 std::ptr::null_mut(),
372 std::ptr::null(),
373 Some(monitor_enum_proc),
374 ptr as isize,
375 );
376 }
377 out
378 }
379
380 unsafe extern "system" fn monitor_enum_proc(
381 hmon: HMONITOR,
382 _hdc: HDC,
383 _rect: *mut RECT,
384 lparam: LPARAM,
385 ) -> BOOL {
386 let mut info: MONITORINFO = std::mem::zeroed();
387 info.cbSize = std::mem::size_of::<MONITORINFO>() as u32;
388 if GetMonitorInfoW(hmon, &mut info) != 0 {
389 let r = info.rcMonitor;
390 let out = &mut *(lparam as *mut Vec<Rect>);
391 out.push(Rect {
392 x: r.left,
393 y: r.top,
394 w: r.right - r.left,
395 h: r.bottom - r.top,
396 });
397 }
398 TRUE
399 }
400
401 fn enumerate_visible_windows() -> Vec<Rect> {
402 let mut out: Vec<Rect> = Vec::new();
403 let ptr: *mut Vec<Rect> = &mut out;
404 unsafe {
407 EnumWindows(Some(enum_windows_proc), ptr as isize);
408 }
409 out
410 }
411
412 unsafe extern "system" fn enum_windows_proc(hwnd: HWND, lparam: LPARAM) -> BOOL {
413 if IsWindowVisible(hwnd) == 0 {
414 return TRUE;
415 }
416 let mut r: RECT = std::mem::zeroed();
417 if GetWindowRect(hwnd, &mut r) == 0 {
418 return TRUE;
419 }
420 let out = &mut *(lparam as *mut Vec<Rect>);
421 out.push(Rect {
422 x: r.left,
423 y: r.top,
424 w: r.right - r.left,
425 h: r.bottom - r.top,
426 });
427 TRUE
428 }
429}
430
431#[cfg(target_os = "linux")]
432mod linux {
433 use std::env;
434 use std::process::Command;
435 use std::sync::atomic::{AtomicBool, Ordering};
436 use std::sync::Arc;
437 use std::sync::OnceLock;
438 use std::thread;
439
440 use super::{pause_decision, WindowKnowledge};
441
442 pub(super) const SYSTEMD_INHIBIT_BIN: &str = "/usr/bin/systemd-inhibit";
446
447 pub(super) const XPROP_BIN: &str = "/usr/bin/xprop";
452
453 pub fn spawn(active: Arc<AtomicBool>) {
454 log_wayland_degradation_once();
455 thread::spawn(move || loop {
456 active.store(check(), Ordering::Relaxed);
457 thread::sleep(super::POLL_INTERVAL);
458 });
459 }
460
461 fn check() -> bool {
462 let assertion = inhibitor_active();
463 if !assertion {
464 return false;
465 }
466 let window = if is_wayland_session() {
470 WindowKnowledge::Unknowable
471 } else {
472 WindowKnowledge::Fullscreen(fullscreen_window_present())
473 };
474 pause_decision(assertion, window)
475 }
476
477 fn inhibitor_active() -> bool {
478 let Ok(output) = Command::new(SYSTEMD_INHIBIT_BIN)
479 .args(["--list", "--no-pager", "--no-legend"])
480 .output()
481 else {
482 return false;
483 };
484 if !output.status.success() {
485 return false;
486 }
487 let Ok(text) = std::str::from_utf8(&output.stdout) else {
488 return false;
489 };
490 parse_idle_inhibitor(text)
491 }
492
493 pub(super) fn parse_idle_inhibitor(text: &str) -> bool {
494 for line in text.lines() {
499 for token in line.split_whitespace() {
500 if token.split(':').any(|w| w == "idle") {
501 return true;
502 }
503 }
504 }
505 false
506 }
507
508 fn is_wayland_session() -> bool {
509 env::var("XDG_SESSION_TYPE")
510 .map(|s| s.eq_ignore_ascii_case("wayland"))
511 .unwrap_or(false)
512 || env::var("WAYLAND_DISPLAY").is_ok()
513 }
514
515 fn log_wayland_degradation_once() {
516 static LOGGED: OnceLock<()> = OnceLock::new();
517 if is_wayland_session() {
518 LOGGED.get_or_init(|| {
519 log::info!(
520 "video: Wayland detected — falling back to assertion-only \
521 (no portable way to enumerate fullscreen windows on Wayland)"
522 );
523 });
524 }
525 }
526
527 fn fullscreen_window_present() -> bool {
528 let Some(active_id) = xprop_active_window_id() else {
529 return false;
530 };
531 let Some(state) = xprop_window_state(&active_id) else {
532 return false;
533 };
534 parse_net_wm_state_fullscreen(&state)
535 }
536
537 fn xprop_active_window_id() -> Option<String> {
538 let out = Command::new(XPROP_BIN)
539 .args(["-root", "_NET_ACTIVE_WINDOW"])
540 .output()
541 .ok()?;
542 if !out.status.success() {
543 return None;
544 }
545 let text = std::str::from_utf8(&out.stdout).ok()?;
546 parse_active_window_id(text)
547 }
548
549 pub(super) fn parse_active_window_id(text: &str) -> Option<String> {
550 let (_, after) = text.trim().rsplit_once('#')?;
554 let id = after.trim();
555 if !id.starts_with("0x") || id == "0x0" {
556 return None;
557 }
558 Some(id.to_string())
559 }
560
561 fn xprop_window_state(id: &str) -> Option<String> {
562 let out = Command::new(XPROP_BIN)
563 .args(["-id", id, "_NET_WM_STATE"])
564 .output()
565 .ok()?;
566 if !out.status.success() {
567 return None;
568 }
569 std::str::from_utf8(&out.stdout).ok().map(str::to_string)
570 }
571
572 pub(super) fn parse_net_wm_state_fullscreen(text: &str) -> bool {
577 text.contains("_NET_WM_STATE_FULLSCREEN")
578 }
579}
580
581#[cfg(test)]
582mod tests {
583 use super::*;
584
585 #[test]
586 fn rect_matches_exact() {
587 let r = Rect {
588 x: 0,
589 y: 0,
590 w: 1920,
591 h: 1080,
592 };
593 assert!(rect_matches(r, r));
594 }
595
596 #[test]
597 fn rect_matches_within_tolerance() {
598 let win = Rect {
599 x: 1,
600 y: 0,
601 w: 1919,
602 h: 1081,
603 };
604 let mon = Rect {
605 x: 0,
606 y: 0,
607 w: 1920,
608 h: 1080,
609 };
610 assert!(rect_matches(win, mon));
611 }
612
613 #[test]
614 fn rect_does_not_match_outside_tolerance() {
615 let win = Rect {
616 x: 0,
617 y: 0,
618 w: 1910,
619 h: 1080,
620 };
621 let mon = Rect {
622 x: 0,
623 y: 0,
624 w: 1920,
625 h: 1080,
626 };
627 assert!(!rect_matches(win, mon));
628 }
629
630 #[test]
631 fn any_window_is_fullscreen_finds_match_across_multiple_monitors() {
632 let windows = [
633 Rect {
634 x: 0,
635 y: 0,
636 w: 800,
637 h: 600,
638 }, Rect {
640 x: 1920,
641 y: 0,
642 w: 2560,
643 h: 1440,
644 }, ];
646 let monitors = [
647 Rect {
648 x: 0,
649 y: 0,
650 w: 1920,
651 h: 1080,
652 },
653 Rect {
654 x: 1920,
655 y: 0,
656 w: 2560,
657 h: 1440,
658 },
659 ];
660 assert!(any_window_is_fullscreen(&windows, &monitors));
661 }
662
663 #[test]
664 fn any_window_is_fullscreen_false_when_nothing_matches() {
665 let windows = [
666 Rect {
667 x: 100,
668 y: 100,
669 w: 800,
670 h: 600,
671 },
672 Rect {
673 x: 0,
674 y: 0,
675 w: 1280,
676 h: 720,
677 },
678 ];
679 let monitors = [Rect {
680 x: 0,
681 y: 0,
682 w: 1920,
683 h: 1080,
684 }];
685 assert!(!any_window_is_fullscreen(&windows, &monitors));
686 }
687
688 #[test]
689 fn any_window_is_fullscreen_false_for_maximised_but_not_fullscreen() {
690 let windows = [Rect {
693 x: 0,
694 y: 0,
695 w: 1920,
696 h: 1040,
697 }];
698 let monitors = [Rect {
699 x: 0,
700 y: 0,
701 w: 1920,
702 h: 1080,
703 }];
704 assert!(!any_window_is_fullscreen(&windows, &monitors));
705 }
706
707 #[test]
713 fn pause_decision_no_pause_without_assertion() {
714 assert!(!pause_decision(false, WindowKnowledge::Fullscreen(true)));
715 assert!(!pause_decision(false, WindowKnowledge::Fullscreen(false)));
716 assert!(!pause_decision(false, WindowKnowledge::Unknowable));
717 }
718
719 #[test]
720 fn pause_decision_pauses_when_assertion_and_fullscreen() {
721 assert!(pause_decision(true, WindowKnowledge::Fullscreen(true)));
722 }
723
724 #[test]
725 fn pause_decision_does_not_pause_for_small_window_video() {
726 assert!(!pause_decision(true, WindowKnowledge::Fullscreen(false)));
729 }
730
731 #[test]
732 fn pause_decision_falls_back_to_assertion_only_when_window_unknowable() {
733 assert!(pause_decision(true, WindowKnowledge::Unknowable));
736 }
737}
738
739#[cfg(all(test, target_os = "macos"))]
740mod macos_tests {
741 use super::macos::{parse_display_sleep_blocked, PMSET_BIN};
742
743 #[test]
744 fn pmset_bin_is_absolute_and_non_empty() {
745 assert!(!PMSET_BIN.is_empty());
746 assert!(
747 PMSET_BIN.starts_with('/'),
748 "expected absolute path, got {PMSET_BIN}"
749 );
750 }
751
752 #[test]
753 fn no_assertions_means_inactive() {
754 let sample = "Assertion status system-wide:\n PreventUserIdleDisplaySleep 0\n UserIsActive 1\n";
755 assert!(!parse_display_sleep_blocked(sample));
756 }
757
758 #[test]
759 fn nonzero_count_means_active() {
760 let sample = "Assertion status system-wide:\n PreventUserIdleDisplaySleep 1\n UserIsActive 1\n";
761 assert!(parse_display_sleep_blocked(sample));
762 }
763
764 #[test]
765 fn higher_counts_still_active() {
766 let sample = " PreventUserIdleDisplaySleep 3\n";
767 assert!(parse_display_sleep_blocked(sample));
768 }
769
770 #[test]
771 fn missing_key_means_inactive() {
772 let sample = "Assertion status system-wide:\n UserIsActive 1\n";
773 assert!(!parse_display_sleep_blocked(sample));
774 }
775
776 #[test]
777 fn garbled_count_means_inactive() {
778 let sample = " PreventUserIdleDisplaySleep NaN\n";
779 assert!(!parse_display_sleep_blocked(sample));
780 }
781}
782
783#[cfg(all(test, target_os = "windows"))]
784mod windows_tests {
785 use super::windows::{parse_display_request, POWERCFG_BIN};
786
787 #[test]
788 fn powercfg_bin_is_absolute_and_non_empty() {
789 assert!(!POWERCFG_BIN.is_empty());
790 assert!(
793 POWERCFG_BIN.contains(":\\"),
794 "expected absolute Windows path, got {POWERCFG_BIN}"
795 );
796 }
797
798 #[test]
799 fn all_none_means_inactive() {
800 let sample = "DISPLAY:\nNone.\n\nSYSTEM:\nNone.\n\nAWAYMODE:\nNone.\n";
801 assert!(!parse_display_request(sample));
802 }
803
804 #[test]
805 fn display_process_means_active() {
806 let sample =
807 "DISPLAY:\n[PROCESS] \\Device\\HarddiskVolume3\\firefox.exe\n\nSYSTEM:\nNone.\n";
808 assert!(parse_display_request(sample));
809 }
810
811 #[test]
812 fn only_system_request_is_inactive() {
813 let sample = "DISPLAY:\nNone.\n\nSYSTEM:\n[DRIVER] Realtek HD Audio\n";
814 assert!(!parse_display_request(sample));
815 }
816}
817
818#[cfg(all(test, target_os = "linux"))]
819mod linux_tests {
820 use super::linux::{parse_active_window_id, parse_idle_inhibitor, SYSTEMD_INHIBIT_BIN};
821
822 #[test]
823 fn systemd_inhibit_bin_is_absolute_and_non_empty() {
824 assert!(!SYSTEMD_INHIBIT_BIN.is_empty());
825 assert!(
826 SYSTEMD_INHIBIT_BIN.starts_with('/'),
827 "expected absolute path, got {SYSTEMD_INHIBIT_BIN}"
828 );
829 }
830
831 #[test]
832 fn empty_means_inactive() {
833 assert!(!parse_idle_inhibitor(""));
834 }
835
836 #[test]
837 fn idle_what_means_active() {
838 let sample = "user 1000 alice 12345 firefox idle Playing:video block\n";
839 assert!(parse_idle_inhibitor(sample));
840 }
841
842 #[test]
843 fn compound_what_with_idle_means_active() {
844 let sample = "user 1000 alice 12345 vlc sleep:idle Playing block\n";
845 assert!(parse_idle_inhibitor(sample));
846 }
847
848 #[test]
849 fn non_idle_inhibitor_is_inactive() {
850 let sample = "user 1000 alice 12345 systemd-logind handle-power-key:handle-suspend-key Lid closed block\n";
851 assert!(!parse_idle_inhibitor(sample));
852 }
853
854 #[test]
855 fn substring_idle_does_not_match() {
856 let sample = "user 1000 alice 12345 daemon sleep Process-is-idle-checker block\n";
857 assert!(!parse_idle_inhibitor(sample));
858 }
859
860 #[test]
861 fn parse_active_window_id_extracts_hex_id() {
862 let sample = "_NET_ACTIVE_WINDOW(WINDOW): window id # 0x3c00006\n";
863 assert_eq!(
864 parse_active_window_id(sample),
865 Some("0x3c00006".to_string())
866 );
867 }
868
869 #[test]
870 fn parse_active_window_id_rejects_non_hex() {
871 let sample = "_NET_ACTIVE_WINDOW(WINDOW): not set\n";
872 assert_eq!(parse_active_window_id(sample), None);
873 }
874
875 #[test]
876 fn parse_active_window_id_rejects_zero_sentinel() {
877 let sample = "_NET_ACTIVE_WINDOW(WINDOW): window id # 0x0\n";
881 assert_eq!(parse_active_window_id(sample), None);
882 }
883
884 use super::linux::parse_net_wm_state_fullscreen;
885
886 #[test]
887 fn parse_net_wm_state_true_when_fullscreen_present() {
888 let sample = "_NET_WM_STATE(ATOM) = _NET_WM_STATE_FULLSCREEN\n";
889 assert!(parse_net_wm_state_fullscreen(sample));
890 }
891
892 #[test]
893 fn parse_net_wm_state_true_when_fullscreen_combined_with_other_states() {
894 let sample = "_NET_WM_STATE(ATOM) = _NET_WM_STATE_FULLSCREEN, _NET_WM_STATE_ABOVE\n";
896 assert!(parse_net_wm_state_fullscreen(sample));
897 }
898
899 #[test]
900 fn parse_net_wm_state_false_for_maximised() {
901 let sample =
905 "_NET_WM_STATE(ATOM) = _NET_WM_STATE_MAXIMIZED_HORZ, _NET_WM_STATE_MAXIMIZED_VERT\n";
906 assert!(!parse_net_wm_state_fullscreen(sample));
907 }
908
909 #[test]
910 fn parse_net_wm_state_false_when_property_missing() {
911 let sample = "_NET_WM_STATE: not found.\n";
913 assert!(!parse_net_wm_state_fullscreen(sample));
914 }
915
916 #[test]
917 fn parse_net_wm_state_false_for_empty_output() {
918 assert!(!parse_net_wm_state_fullscreen(""));
919 }
920}