#![warn(missing_debug_implementations)]
mod error;
#[cfg(test)]
mod tests;
pub use crate::error::Error;
pub use crate::error::ErrorKind;
use crate::error::Repr;
pub use crate::error::Result;
use std::fmt;
use std::os::windows::io::AsHandle;
use std::os::windows::io::AsRawHandle;
use std::os::windows::io::BorrowedHandle;
use std::os::windows::io::FromRawHandle;
use std::os::windows::io::OwnedHandle;
use std::os::windows::io::RawHandle;
use std::ptr;
use std::time::Duration;
use win_nightlight_lib::Nightlight;
use win_nightlight_lib::SETTINGS_KEY_PATH;
use win_nightlight_lib::STATE_KEY_PATH;
use win_nightlight_lib::Settings;
use win_nightlight_lib::State;
use windows_registry::CURRENT_USER;
use windows_registry::Key;
use windows_result::WIN32_ERROR;
use windows_sys::Win32::Foundation::WAIT_FAILED;
use windows_sys::Win32::Foundation::WAIT_OBJECT_0;
use windows_sys::Win32::Foundation::WAIT_TIMEOUT;
use windows_sys::Win32::System::Registry::KEY_NOTIFY;
use windows_sys::Win32::System::Registry::REG_NOTIFY_CHANGE_LAST_SET;
use windows_sys::Win32::System::Registry::REG_NOTIFY_THREAD_AGNOSTIC;
use windows_sys::Win32::System::Registry::RegNotifyChangeKeyValue;
use windows_sys::Win32::System::Threading::CreateEventW;
use windows_sys::Win32::System::Threading::INFINITE;
use windows_sys::Win32::System::Threading::ResetEvent;
use windows_sys::Win32::System::Threading::WaitForSingleObject;
const MAX_TIMEOUT_MILLIS: u32 = INFINITE - 1;
#[derive(Debug, Clone)]
pub enum Snapshot {
Settings(Settings),
State(State),
}
pub struct Watcher {
key: Key,
event: OwnedHandle,
read: Box<dyn Fn() -> win_nightlight_lib::Result<Snapshot> + Send>,
}
impl Watcher {
pub fn settings() -> Result<Self> {
let nightlight = Nightlight::new();
Self::open(SETTINGS_KEY_PATH, move || {
nightlight.read_settings().map(Snapshot::Settings)
})
}
pub fn state() -> Result<Self> {
let nightlight = Nightlight::new();
Self::open(STATE_KEY_PATH, move || {
nightlight.read_state().map(Snapshot::State)
})
}
pub fn read_current(&self) -> Result<Snapshot> {
(self.read)().map_err(|source| Repr::Read { source }.into())
}
pub fn wait_timeout(&self, duration: Duration) -> Result<Option<Snapshot>> {
if self.consume_notification(duration)? {
self.read_current().map(Some)
} else {
Ok(None)
}
}
pub fn read_pending(&self) -> Result<Option<Snapshot>> {
self.wait_timeout(Duration::ZERO)
}
pub fn discard_pending(&self) -> Result<bool> {
self.consume_notification(Duration::ZERO)
}
#[expect(unsafe_code, reason = "create and take ownership of a Win32 event")]
fn open(
path: &str,
read: impl Fn() -> win_nightlight_lib::Result<Snapshot> + Send + 'static,
) -> Result<Self> {
let key = CURRENT_USER
.options()
.access(KEY_NOTIFY)
.open(path)
.map_err(|source| Repr::Open { source })?;
let event = unsafe { CreateEventW(ptr::null(), 1, 0, ptr::null()) };
if event.is_null() {
return Err(Repr::CreateEvent {
source: windows_result::Error::from_thread(),
}
.into());
}
let event = unsafe { OwnedHandle::from_raw_handle(event) };
let watcher = Self {
key,
event,
read: Box::new(read),
};
watcher.rearm()?;
Ok(watcher)
}
#[expect(unsafe_code, reason = "register an owned key with its owned event")]
fn rearm(&self) -> Result<()> {
let status = unsafe {
RegNotifyChangeKeyValue(
self.key.as_raw(),
0,
REG_NOTIFY_CHANGE_LAST_SET | REG_NOTIFY_THREAD_AGNOSTIC,
self.event.as_raw_handle(),
1,
)
};
WIN32_ERROR(status)
.ok()
.map_err(|source| Repr::Register { source }.into())
}
#[expect(unsafe_code, reason = "wait on and reset the watcher-owned event")]
fn consume_notification(&self, duration: Duration) -> Result<bool> {
let milliseconds = timeout_millis(duration)?;
let status = unsafe { WaitForSingleObject(self.event.as_raw_handle(), milliseconds) };
match status {
WAIT_OBJECT_0 => {
if unsafe { ResetEvent(self.event.as_raw_handle()) } == 0 {
return Err(Repr::ResetEvent {
source: windows_result::Error::from_thread(),
}
.into());
}
self.rearm()?;
Ok(true)
}
WAIT_TIMEOUT => Ok(false),
WAIT_FAILED => Err(Repr::Wait {
source: windows_result::Error::from_thread(),
}
.into()),
other => Err(Repr::WaitStatus(other).into()),
}
}
}
impl fmt::Debug for Watcher {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("Watcher")
.field("key", &self.key)
.field("event", &self.event)
.finish_non_exhaustive()
}
}
impl AsHandle for Watcher {
fn as_handle(&self) -> BorrowedHandle<'_> {
self.event.as_handle()
}
}
impl AsRawHandle for Watcher {
fn as_raw_handle(&self) -> RawHandle {
self.event.as_raw_handle()
}
}
fn timeout_millis(duration: Duration) -> Result<u32> {
let milliseconds =
u32::try_from(duration.as_nanos().div_ceil(1_000_000)).map_err(|_error| Repr::Timeout)?;
if milliseconds > MAX_TIMEOUT_MILLIS {
return Err(Repr::Timeout.into());
}
Ok(milliseconds)
}