#[cfg(target_os = "macos")]
mod macos;
use std::sync::Arc;
use std::sync::atomic::{AtomicU8, Ordering};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
enum Reason {
ScreensAsleep,
SessionInactive,
ScreenLocked,
}
impl Reason {
fn bit(self) -> u8 {
match self {
Reason::ScreensAsleep => 1,
Reason::SessionInactive => 1 << 1,
Reason::ScreenLocked => 1 << 2,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
struct Reading {
screens_asleep: Option<bool>,
on_console: Option<bool>,
locked: Option<bool>,
}
#[derive(Debug, Default)]
struct Reasons(AtomicU8);
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
impl Reasons {
fn set(&self, reason: Reason, on: bool) -> bool {
let bit = reason.bit();
let before = if on {
self.0.fetch_or(bit, Ordering::AcqRel)
} else {
self.0.fetch_and(!bit, Ordering::AcqRel)
};
let after = if on { before | bit } else { before & !bit };
(before != 0) != (after != 0)
}
fn away(&self) -> bool {
self.0.load(Ordering::Acquire) != 0
}
fn recheck(&self, reading: Reading) -> bool {
let mut changed = false;
if reading.screens_asleep == Some(false) {
changed |= self.set(Reason::ScreensAsleep, false);
}
if reading.on_console == Some(true) {
changed |= self.set(Reason::SessionInactive, false);
}
if reading.locked == Some(false) {
changed |= self.set(Reason::ScreenLocked, false);
}
changed
}
}
pub struct SessionWatch {
reasons: Arc<Reasons>,
#[cfg(target_os = "macos")]
_observer: macos::Observer,
}
impl std::fmt::Debug for SessionWatch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SessionWatch")
.field("away", &self.away())
.finish()
}
}
impl SessionWatch {
pub fn new(changed: impl Fn() + Send + Sync + 'static) -> Option<Self> {
#[cfg(target_os = "macos")]
{
let reasons = Arc::new(Reasons::default());
let observer = macos::Observer::new(Arc::clone(&reasons), Box::new(changed))?;
Some(Self {
reasons,
_observer: observer,
})
}
#[cfg(not(target_os = "macos"))]
{
let _ = changed;
None
}
}
pub fn away(&self) -> bool {
self.reasons.away()
}
pub fn recheck(&self) -> bool {
#[cfg(target_os = "macos")]
{
self.reasons.recheck(macos::reading())
}
#[cfg(not(target_os = "macos"))]
{
false
}
}
}
#[cfg(test)]
mod tests {
use super::{Reading, Reason, Reasons};
#[test]
fn away_while_any_reason_holds() {
let reasons = Reasons::default();
assert!(!reasons.away());
assert!(reasons.set(Reason::ScreensAsleep, true));
assert!(!reasons.set(Reason::ScreenLocked, true), "already away");
assert!(!reasons.set(Reason::ScreensAsleep, true), "set twice");
assert!(!reasons.set(Reason::ScreensAsleep, false), "still locked");
assert!(reasons.away());
assert!(reasons.set(Reason::ScreenLocked, false));
assert!(!reasons.away());
assert!(!reasons.set(Reason::SessionInactive, false), "never set");
}
#[test]
fn a_reading_clears_and_never_sets() {
let reasons = Reasons::default();
let everything = Reading {
screens_asleep: Some(true),
on_console: Some(false),
locked: Some(true),
};
assert!(!reasons.recheck(everything));
assert!(!reasons.away(), "a reading sets nothing");
reasons.set(Reason::ScreensAsleep, true);
reasons.set(Reason::ScreenLocked, true);
assert!(!reasons.recheck(Reading::default()), "it cannot say");
let awake = Reading {
screens_asleep: Some(false),
..Reading::default()
};
assert!(!reasons.recheck(awake), "the screen is still locked");
assert!(reasons.away());
let unlocked = Reading {
locked: Some(false),
..Reading::default()
};
assert!(reasons.recheck(unlocked));
assert!(!reasons.away());
reasons.set(Reason::SessionInactive, true);
let console = Reading {
on_console: Some(true),
..Reading::default()
};
assert!(reasons.recheck(console));
assert!(!reasons.away());
}
}