win_nightlight_watch/lib.rs
1//! Observe Night Light changes and read decoded snapshots.
2//!
3//! Create one [`Watcher`] for settings and one for state as needed. Read each
4//! initial snapshot with [`Watcher::read_current`] after constructing it.
5//! Successful notification consumption rearms before reading. Snapshots reflect
6//! the later read, and notifications can combine writes or produce duplicates.
7//!
8//! The watcher does not start a thread or write registry values. This crate
9//! supports Windows only. Fallible operations return an [`Error`]; public
10//! operations do not panic.
11//!
12//! # Example
13//!
14//! ```no_run
15//! use std::time::Duration;
16//! use win_nightlight_watch::Snapshot;
17//! use win_nightlight_watch::Watcher;
18//!
19//! fn observe(mut changed: impl FnMut(Snapshot)) -> Result<(), Box<dyn std::error::Error>> {
20//! let watcher = Watcher::settings()?;
21//! changed(watcher.read_current()?);
22//!
23//! loop {
24//! if let Some(snapshot) = watcher.wait_timeout(Duration::from_secs(60))? {
25//! changed(snapshot);
26//! }
27//! }
28//! }
29//! ```
30
31#![warn(missing_debug_implementations)]
32
33mod error;
34#[cfg(test)]
35mod tests;
36
37pub use crate::error::Error;
38pub use crate::error::ErrorKind;
39use crate::error::Repr;
40pub use crate::error::Result;
41use std::fmt;
42use std::os::windows::io::AsHandle;
43use std::os::windows::io::AsRawHandle;
44use std::os::windows::io::BorrowedHandle;
45use std::os::windows::io::FromRawHandle;
46use std::os::windows::io::OwnedHandle;
47use std::os::windows::io::RawHandle;
48use std::ptr;
49use std::time::Duration;
50use win_nightlight_lib::Nightlight;
51use win_nightlight_lib::SETTINGS_KEY_PATH;
52use win_nightlight_lib::STATE_KEY_PATH;
53use win_nightlight_lib::Settings;
54use win_nightlight_lib::State;
55use windows_registry::CURRENT_USER;
56use windows_registry::Key;
57use windows_result::WIN32_ERROR;
58use windows_sys::Win32::Foundation::WAIT_FAILED;
59use windows_sys::Win32::Foundation::WAIT_OBJECT_0;
60use windows_sys::Win32::Foundation::WAIT_TIMEOUT;
61use windows_sys::Win32::System::Registry::KEY_NOTIFY;
62use windows_sys::Win32::System::Registry::REG_NOTIFY_CHANGE_LAST_SET;
63use windows_sys::Win32::System::Registry::REG_NOTIFY_THREAD_AGNOSTIC;
64use windows_sys::Win32::System::Registry::RegNotifyChangeKeyValue;
65use windows_sys::Win32::System::Threading::CreateEventW;
66use windows_sys::Win32::System::Threading::INFINITE;
67use windows_sys::Win32::System::Threading::ResetEvent;
68use windows_sys::Win32::System::Threading::WaitForSingleObject;
69
70const MAX_TIMEOUT_MILLIS: u32 = INFINITE - 1;
71
72/// Store a decoded observation of one Night Light registry value.
73///
74/// The value reflects the read, not necessarily the write that signaled the
75/// watcher. Settings and state are observed independently.
76#[derive(Debug, Clone)]
77pub enum Snapshot {
78 /// Settings read by a watcher created with [`Watcher::settings`].
79 Settings(Settings),
80 /// State read by a watcher created with [`Watcher::state`].
81 State(State),
82}
83
84/// Observe one Night Light record without a background thread.
85///
86/// - Use one notification consumer. `Watcher` is `Send` but not `Sync`.
87/// - External waits observe readiness without consuming it. Then call
88/// [`read_pending`](Self::read_pending) or
89/// [`discard_pending`](Self::discard_pending).
90/// - Do not reset or signal the exposed event yourself.
91/// - Drop the watcher after a notification failure. An [`ErrorKind::Read`]
92/// failure preserves registration and permits another read.
93pub struct Watcher {
94 // Closing the key can signal the event, so the key must drop first.
95 key: Key,
96 event: OwnedHandle,
97 read: Box<dyn Fn() -> win_nightlight_lib::Result<Snapshot> + Send>,
98}
99
100impl Watcher {
101 /// Register for Night Light settings changes without reading an initial
102 /// snapshot.
103 ///
104 /// # Errors
105 ///
106 /// - [`ErrorKind::NotFound`] if the settings key does not exist.
107 /// - [`ErrorKind::KeyDeleted`] if Windows reports a deleted key.
108 /// - [`ErrorKind::Windows`] if opening, event creation, or registration
109 /// fails.
110 pub fn settings() -> Result<Self> {
111 let nightlight = Nightlight::new();
112 Self::open(SETTINGS_KEY_PATH, move || {
113 nightlight.read_settings().map(Snapshot::Settings)
114 })
115 }
116
117 /// Register for Night Light state changes without reading an initial
118 /// snapshot.
119 ///
120 /// # Errors
121 ///
122 /// - [`ErrorKind::NotFound`] if the state key does not exist.
123 /// - [`ErrorKind::KeyDeleted`] if Windows reports a deleted key.
124 /// - [`ErrorKind::Windows`] if opening, event creation, or registration
125 /// fails.
126 pub fn state() -> Result<Self> {
127 let nightlight = Nightlight::new();
128 Self::open(STATE_KEY_PATH, move || {
129 nightlight.read_state().map(Snapshot::State)
130 })
131 }
132
133 /// Read the current record without consuming notifications or changing
134 /// registration.
135 ///
136 /// Use for the initial snapshot or an independently scheduled retry.
137 ///
138 /// # Errors
139 ///
140 /// Return [`ErrorKind::Read`] with the underlying library error if reading
141 /// or decoding fails. The watcher remains registered.
142 pub fn read_current(&self) -> Result<Snapshot> {
143 (self.read)().map_err(|source| Repr::Read { source }.into())
144 }
145
146 /// Wait for a notification, rearm, and return a freshly read snapshot.
147 ///
148 /// - Return `None` on timeout, without reading or changing registration.
149 /// - The duration bounds the native wait, not the subsequent snapshot read.
150 /// - Zero polls; positive fractional milliseconds round up. Time spent
151 /// asleep does not count toward the wait timeout on Windows 11.
152 /// - Message-processing threads should use an external message-aware wait
153 /// followed by [`read_pending`](Self::read_pending).
154 ///
155 /// # Errors
156 ///
157 /// - [`ErrorKind::InvalidInput`] above 4,294,967,294 milliseconds, without
158 /// consuming a notification.
159 /// - [`ErrorKind::Read`] if the snapshot read fails after successful
160 /// rearming.
161 /// - Drop the watcher after any other error; notification handling failed.
162 pub fn wait_timeout(&self, duration: Duration) -> Result<Option<Snapshot>> {
163 if self.consume_notification(duration)? {
164 self.read_current().map(Some)
165 } else {
166 Ok(None)
167 }
168 }
169
170 /// Consume a pending notification, rearm, and read a snapshot without
171 /// waiting for a change.
172 ///
173 /// Return `None` without reading or rearming if no notification is pending.
174 /// Use after an external wait on the watcher event.
175 ///
176 /// # Errors
177 ///
178 /// Return the errors of [`wait_timeout`](Self::wait_timeout).
179 pub fn read_pending(&self) -> Result<Option<Snapshot>> {
180 self.wait_timeout(Duration::ZERO)
181 }
182
183 /// Consume a pending notification and rearm without reading a snapshot.
184 ///
185 /// Return whether a notification was consumed. Use when snapshot reads
186 /// must be deferred; call [`read_current`](Self::read_current) to retry
187 /// later.
188 ///
189 /// # Errors
190 ///
191 /// Drop the watcher after any error; waiting, resetting, or registration
192 /// failed.
193 pub fn discard_pending(&self) -> Result<bool> {
194 self.consume_notification(Duration::ZERO)
195 }
196
197 #[expect(unsafe_code, reason = "create and take ownership of a Win32 event")]
198 fn open(
199 path: &str,
200 read: impl Fn() -> win_nightlight_lib::Result<Snapshot> + Send + 'static,
201 ) -> Result<Self> {
202 let key = CURRENT_USER
203 .options()
204 .access(KEY_NOTIFY)
205 .open(path)
206 .map_err(|source| Repr::Open { source })?;
207 // SAFETY: Null attributes and name request an unnamed, non-inheritable
208 // event. Manual reset keeps external waits from consuming readiness.
209 let event = unsafe { CreateEventW(ptr::null(), 1, 0, ptr::null()) };
210 if event.is_null() {
211 return Err(Repr::CreateEvent {
212 source: windows_result::Error::from_thread(),
213 }
214 .into());
215 }
216 // SAFETY: CreateEventW returned a valid, uniquely owned handle closed
217 // by CloseHandle. Ownership transfers here exactly once.
218 let event = unsafe { OwnedHandle::from_raw_handle(event) };
219 let watcher = Self {
220 key,
221 event,
222 read: Box::new(read),
223 };
224 watcher.rearm()?;
225 Ok(watcher)
226 }
227
228 #[expect(unsafe_code, reason = "register an owned key with its owned event")]
229 fn rearm(&self) -> Result<()> {
230 // SAFETY: Both handles remain open throughout registration. The key
231 // has KEY_NOTIFY access; construction or consumption precedes this
232 // call.
233 let status = unsafe {
234 RegNotifyChangeKeyValue(
235 self.key.as_raw(),
236 0,
237 REG_NOTIFY_CHANGE_LAST_SET | REG_NOTIFY_THREAD_AGNOSTIC,
238 self.event.as_raw_handle(),
239 1,
240 )
241 };
242 WIN32_ERROR(status)
243 .ok()
244 .map_err(|source| Repr::Register { source }.into())
245 }
246
247 #[expect(unsafe_code, reason = "wait on and reset the watcher-owned event")]
248 fn consume_notification(&self, duration: Duration) -> Result<bool> {
249 let milliseconds = timeout_millis(duration)?;
250 // SAFETY: The watcher keeps its event open throughout this wait.
251 let status = unsafe { WaitForSingleObject(self.event.as_raw_handle(), milliseconds) };
252 match status {
253 WAIT_OBJECT_0 => {
254 // SAFETY: The event is valid and has one consumer. Reset before
255 // rearming so a subsequent notification cannot be erased.
256 if unsafe { ResetEvent(self.event.as_raw_handle()) } == 0 {
257 return Err(Repr::ResetEvent {
258 source: windows_result::Error::from_thread(),
259 }
260 .into());
261 }
262 self.rearm()?;
263 Ok(true)
264 }
265 WAIT_TIMEOUT => Ok(false),
266 WAIT_FAILED => Err(Repr::Wait {
267 source: windows_result::Error::from_thread(),
268 }
269 .into()),
270 other => Err(Repr::WaitStatus(other).into()),
271 }
272 }
273}
274
275impl fmt::Debug for Watcher {
276 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
277 formatter
278 .debug_struct("Watcher")
279 .field("key", &self.key)
280 .field("event", &self.event)
281 .finish_non_exhaustive()
282 }
283}
284
285impl AsHandle for Watcher {
286 fn as_handle(&self) -> BorrowedHandle<'_> {
287 self.event.as_handle()
288 }
289}
290
291impl AsRawHandle for Watcher {
292 fn as_raw_handle(&self) -> RawHandle {
293 self.event.as_raw_handle()
294 }
295}
296
297fn timeout_millis(duration: Duration) -> Result<u32> {
298 let milliseconds =
299 u32::try_from(duration.as_nanos().div_ceil(1_000_000)).map_err(|_error| Repr::Timeout)?;
300 if milliseconds > MAX_TIMEOUT_MILLIS {
301 return Err(Repr::Timeout.into());
302 }
303 Ok(milliseconds)
304}