Skip to main content

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}