teksilo_platform/
session.rs1#[cfg(target_os = "macos")]
29mod macos;
30
31use std::sync::Arc;
32use std::sync::atomic::{AtomicU8, Ordering};
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
37enum Reason {
38 ScreensAsleep,
40 SessionInactive,
42 ScreenLocked,
44}
45
46impl Reason {
47 fn bit(self) -> u8 {
48 match self {
49 Reason::ScreensAsleep => 1,
50 Reason::SessionInactive => 1 << 1,
51 Reason::ScreenLocked => 1 << 2,
52 }
53 }
54}
55
56#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
58#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
59struct Reading {
60 screens_asleep: Option<bool>,
61 on_console: Option<bool>,
62 locked: Option<bool>,
63}
64
65#[derive(Debug, Default)]
68struct Reasons(AtomicU8);
69
70#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
71impl Reasons {
72 fn set(&self, reason: Reason, on: bool) -> bool {
74 let bit = reason.bit();
75 let before = if on {
76 self.0.fetch_or(bit, Ordering::AcqRel)
77 } else {
78 self.0.fetch_and(!bit, Ordering::AcqRel)
79 };
80 let after = if on { before | bit } else { before & !bit };
81 (before != 0) != (after != 0)
82 }
83
84 fn away(&self) -> bool {
85 self.0.load(Ordering::Acquire) != 0
86 }
87
88 fn recheck(&self, reading: Reading) -> bool {
91 let mut changed = false;
92 if reading.screens_asleep == Some(false) {
93 changed |= self.set(Reason::ScreensAsleep, false);
94 }
95 if reading.on_console == Some(true) {
96 changed |= self.set(Reason::SessionInactive, false);
97 }
98 if reading.locked == Some(false) {
99 changed |= self.set(Reason::ScreenLocked, false);
100 }
101 changed
102 }
103}
104
105pub struct SessionWatch {
108 reasons: Arc<Reasons>,
109 #[cfg(target_os = "macos")]
110 _observer: macos::Observer,
111}
112
113impl std::fmt::Debug for SessionWatch {
114 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115 f.debug_struct("SessionWatch")
116 .field("away", &self.away())
117 .finish()
118 }
119}
120
121impl SessionWatch {
122 pub fn new(changed: impl Fn() + Send + Sync + 'static) -> Option<Self> {
127 #[cfg(target_os = "macos")]
128 {
129 let reasons = Arc::new(Reasons::default());
130 let observer = macos::Observer::new(Arc::clone(&reasons), Box::new(changed))?;
131 Some(Self {
132 reasons,
133 _observer: observer,
134 })
135 }
136 #[cfg(not(target_os = "macos"))]
137 {
138 let _ = changed;
139 None
140 }
141 }
142
143 pub fn away(&self) -> bool {
146 self.reasons.away()
147 }
148
149 pub fn recheck(&self) -> bool {
154 #[cfg(target_os = "macos")]
155 {
156 self.reasons.recheck(macos::reading())
157 }
158 #[cfg(not(target_os = "macos"))]
159 {
160 false
161 }
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::{Reading, Reason, Reasons};
168
169 #[test]
172 fn away_while_any_reason_holds() {
173 let reasons = Reasons::default();
174 assert!(!reasons.away());
175 assert!(reasons.set(Reason::ScreensAsleep, true));
176 assert!(!reasons.set(Reason::ScreenLocked, true), "already away");
177 assert!(!reasons.set(Reason::ScreensAsleep, true), "set twice");
178 assert!(!reasons.set(Reason::ScreensAsleep, false), "still locked");
179 assert!(reasons.away());
180 assert!(reasons.set(Reason::ScreenLocked, false));
181 assert!(!reasons.away());
182 assert!(!reasons.set(Reason::SessionInactive, false), "never set");
183 }
184
185 #[test]
188 fn a_reading_clears_and_never_sets() {
189 let reasons = Reasons::default();
190 let everything = Reading {
191 screens_asleep: Some(true),
192 on_console: Some(false),
193 locked: Some(true),
194 };
195 assert!(!reasons.recheck(everything));
196 assert!(!reasons.away(), "a reading sets nothing");
197
198 reasons.set(Reason::ScreensAsleep, true);
199 reasons.set(Reason::ScreenLocked, true);
200 assert!(!reasons.recheck(Reading::default()), "it cannot say");
201 let awake = Reading {
202 screens_asleep: Some(false),
203 ..Reading::default()
204 };
205 assert!(!reasons.recheck(awake), "the screen is still locked");
206 assert!(reasons.away());
207 let unlocked = Reading {
208 locked: Some(false),
209 ..Reading::default()
210 };
211 assert!(reasons.recheck(unlocked));
212 assert!(!reasons.away());
213
214 reasons.set(Reason::SessionInactive, true);
215 let console = Reading {
216 on_console: Some(true),
217 ..Reading::default()
218 };
219 assert!(reasons.recheck(console));
220 assert!(!reasons.away());
221 }
222}