sem_reg/cloud_store/
night_light.rs

1//! Types to retrieve information about and control the Windows Night Light feature.
2//!
3//! In regular use cases, you should use the `NightLight` type. With the underlying raw types, which are also public, there can be unexpected behavior or inconsistent states that, when written to the registry, may not get resolved until restart of the OS or at least logging off:
4//!
5//! - Changing schedule-related settings - written to the settings registry value - may entail the state registry value - incl. the active-state - being changed by the OS. Trying to change both the settings and state registry value in close temporal proximity in an irreconcilable way may lead to one not being applied.
6//! - Night preview must not be switched or held active while changing anything that may potentially switch the active-state (incl. schedule-related settings).
7//! - After logging on to the OS, changing the color temperature on its own remains ineffective if preview mode wasn't at least once activated in the session. (Briefly activating it also restores the color temperature after turning the screen back on.) If Night Light is off, changing the temperature and then, not in very close temporal proximity, turning Night Light on, correctly applies the temperature in that instance, however.
8//!
9//! With all types, there can be race conditions when simultaneously changing the Night Light settings elsewhere in the system. This is why you should read, mutate and write without delays in between. `NightLight` instances expire after a short duration to enforce this.
10//!
11//! The `NightLight` type encapsulates both the state and settings registry value and only writes one when you de facto changed its properties, compared with the data retrieved on instance creation, failing if changes don't harmonize with other properties (changed or unchanged). Using `NightLight` twice in direct succession won't help you writing both registry values in an irreconcilable way (the error may just be silent). If you need to do that, use a delay between writing a `NightLight` instance to registry and creating the next, causing the state registry value with the active-state to be changed last.
12
13mod settings;
14mod state;
15mod time;
16
17use chrono::SecondsFormat;
18use convert_case::{Case, Casing};
19use core::fmt;
20use futures::channel::oneshot;
21use serde_json::json;
22pub use settings::{RawNightLightSettings, ScheduleType};
23pub use state::{RawNightLightState, TransitionCause};
24use std::{
25    io,
26    ops::Sub,
27    path::Path,
28    thread,
29    time::{Duration, Instant, SystemTime},
30};
31pub use time::{ClockTime, ClockTimeFrame, Meridiem};
32use winreg::{
33    enums::{HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_QUERY_VALUE},
34    RegKey,
35};
36
37use crate::{
38    data_conversion::{
39        format::write_table,
40        time::{
41            epoch_duration_to_filetime, utc_epoch_secs_to_local_iso_string,
42            utc_filetime_to_local_date_time, utc_filetime_to_local_iso_string,
43        },
44        ParseError, Strictness,
45    },
46    reg::{
47        delete_reg_value, export_reg_bin_values,
48        monitor::{MonitorLoopError, RegValueMonitor},
49        read_reg_bin_value, write_reg_bin_value, RegValuePath,
50    },
51};
52
53pub struct NightLight {
54    state: RawNightLightState,
55    settings: RawNightLightSettings,
56    sunset_to_sunrise_possible: Option<bool>,
57    uses_12_hour_clock: bool,
58    loaded_instant: Instant,
59    strictness: Strictness,
60}
61
62impl NightLight {
63    /// The lowest Kelvin value last known to be valid (as of Oct. 2023).
64    pub const MIN_NIGHT_COLOR_TEMP: u16 = 1200;
65    pub const MAX_NIGHT_COLOR_TEMP: u16 = 6500;
66
67    /// Alias for minimum.
68    pub const WARMEST_NIGHT_COLOR_TEMP: u16 = Self::MIN_NIGHT_COLOR_TEMP;
69    /// Alias for maximum.
70    pub const COLDEST_NIGHT_COLOR_TEMP: u16 = Self::MAX_NIGHT_COLOR_TEMP;
71
72    /// Official default as of Dec. 2023. Found out by alternatingly setting the warmth factor to `None` and a concrete value until no difference could be visually perceived anymore, then snapping to a plausible round number.
73    pub const DEFAULT_NIGHT_COLOR_TEMP: u16 = 4000;
74    /// Equivalent of [`Self::DEFAULT_NIGHT_COLOR_TEMP`]. See [`Self::warmth()`].
75    pub const DEFAULT_WARMTH: f32 = 1.0
76        - (Self::DEFAULT_NIGHT_COLOR_TEMP - Self::MIN_NIGHT_COLOR_TEMP) as f32
77            / (Self::MAX_NIGHT_COLOR_TEMP - Self::MIN_NIGHT_COLOR_TEMP) as f32;
78
79    /// The delay [`Self::init_with_strictness()`] should wait for, if no better information is available. (Defined as a common animation duration.)
80    pub const REASONABLE_INIT_DELAY: Duration = Duration::from_millis(200);
81
82    /// Duration after which an instance expires. May be shortened in future versions.
83    pub const EXPIRATION_TIMEOUT: Duration = Duration::from_millis(1000);
84
85    pub fn from_reg() -> Result<Self, self::Error> {
86        //! Creates a strict instance using [`Self::from_reg_with_strictness()`].
87
88        Self::from_reg_with_strictness(Strictness::Strict)
89    }
90
91    pub fn from_reg_lenient() -> Result<Self, self::Error> {
92        Self::from_reg_with_strictness(Strictness::Lenient)
93    }
94
95    pub fn from_reg_with_strictness(strictness: Strictness) -> Result<Self, self::Error> {
96        //! Returns a fallback instance if one of the registry values doesn't exist in lenient mode.
97
98        Ok(match NightLightBytes::from_reg() {
99            Ok(bytes) => Self::from_bytes_with_strictness(bytes, strictness)?,
100            Err(error) => {
101                if error.kind() == io::ErrorKind::NotFound && strictness.is_lenient() {
102                    Self::lenient_fallback()
103                } else {
104                    Err(error)?
105                }
106            }
107        })
108    }
109
110    pub fn from_bytes(bytes: NightLightBytes) -> Result<Self, ParseError> {
111        Self::from_bytes_with_strictness(bytes, Strictness::Strict)
112    }
113
114    pub fn from_bytes_lenient(bytes: NightLightBytes) -> Result<Self, ParseError> {
115        Self::from_bytes_with_strictness(bytes, Strictness::Lenient)
116    }
117
118    pub fn from_bytes_with_strictness(
119        bytes: NightLightBytes,
120        strictness: Strictness,
121    ) -> Result<Self, ParseError> {
122        Ok(Self {
123            state: RawNightLightState::from_bytes(bytes.state, strictness)?,
124            settings: RawNightLightSettings::from_bytes(bytes.settings, strictness)?,
125            sunset_to_sunrise_possible: Self::sunset_to_sunrise_possible(),
126            uses_12_hour_clock: false,
127            loaded_instant: Instant::now(),
128            strictness,
129        })
130    }
131
132    pub fn lenient_fallback() -> Self {
133        let now = SystemTime::now();
134        Self {
135            state: RawNightLightState::lenient_fallback(now),
136            settings: RawNightLightSettings::lenient_fallback(now),
137            sunset_to_sunrise_possible: Self::sunset_to_sunrise_possible(),
138            uses_12_hour_clock: false,
139            loaded_instant: Instant::now(),
140            strictness: Strictness::Lenient,
141        }
142    }
143
144    pub fn init(delay: Duration, also_wait_after: bool) -> Result<(), self::Error> {
145        //! Initializes with strict instances using [`Self::init_with_strictness()`].
146
147        Self::init_with_strictness(delay, also_wait_after, Strictness::Strict)
148    }
149
150    pub fn init_with_strictness(
151        delay: Duration,
152        also_wait_after: bool,
153        strictness: Strictness,
154    ) -> Result<(), self::Error> {
155        //! Performs actions that are necessary after OS log-on to be able to change the color temperature without activating one of the boolean states. Also restores a warm color temperature after turning the screen back on. (As of Dec. 2023.)
156        //!
157        //! Concretely, it activates preview mode, waits for the delay duration, and deactivates it again. Doesn't write a registry value and wait, if preview mode was already active. To make the intermediate previewing as invisible as possible, the color temperature is temporarily set to the coldest possible, if Night Light is inactive.
158        //!
159        //! Normally to be used with [`Self::REASONABLE_INIT_DELAY`]. Additionally waiting after the last write can be activated in a command line context, if the user plans to follow the action up with another write.
160
161        let mut inst = Self::from_reg_with_strictness(strictness)?;
162        if !inst.night_preview_active() {
163            // Activate preview mode.
164            let previous_temp = if inst.active() {
165                None
166            } else {
167                let temp = inst.night_color_temp();
168                inst.set_night_color_temp(Some(Self::COLDEST_NIGHT_COLOR_TEMP)); // Make it invisible.
169                temp
170            };
171            inst.set_night_preview_active(true);
172            inst.write_to_reg()?;
173
174            thread::sleep(delay);
175
176            // Deactivate preview mode.
177            let mut inst = Self::from_reg_with_strictness(strictness)?;
178            if let Some(temp) = previous_temp {
179                inst.set_night_color_temp(Some(temp));
180            };
181            inst.set_night_preview_active(false);
182            inst.write_to_reg()?;
183
184            if also_wait_after {
185                thread::sleep(delay);
186            }
187        }
188
189        Ok(())
190    }
191
192    pub fn export_reg<T: AsRef<Path>>(file_path: T) -> Result<(), io::Error> {
193        //! Writes the Night Light registry values to a file in .reg file format.
194
195        export_reg_bin_values(
196            &[
197                RawNightLightState::REG_VALUE_PATH,
198                RawNightLightSettings::REG_VALUE_PATH,
199            ],
200            file_path,
201        )
202    }
203
204    pub fn delete_reg() -> Result<(), io::Error> {
205        //! Deletes the Night Light registry values to reset the Windows feature. May help when they've been corrupted and Night Light became unusable. User should restart or at least log-off after deletion.
206
207        // Deletion order may be relevant. This order made the fewest problems so far.
208        delete_reg_value(&RawNightLightSettings::REG_VALUE_PATH)?;
209        delete_reg_value(&RawNightLightState::REG_VALUE_PATH)?;
210
211        Ok(())
212    }
213
214    pub fn monitor<F, T, E>(
215        stop_receiver: Option<oneshot::Receiver<T>>,
216        mut callback: F,
217    ) -> Result<T, MonitorLoopError<E>>
218    where
219        F: FnMut(RegValueId) -> Option<Result<T, E>>,
220        T: Default,
221    {
222        let mut monitor = RegValueMonitor::new([
223            (RegValueId::State, &RawNightLightState::REG_VALUE_PATH),
224            (RegValueId::Settings, &RawNightLightSettings::REG_VALUE_PATH),
225        ])?;
226
227        monitor.r#loop(stop_receiver, |value_id| callback(value_id))
228    }
229
230    pub fn sunset_to_sunrise_possible() -> Option<bool> {
231        //! Whether the "Sunset to sunrise" option is available, because location services are turned on. If not, the explicit schedule is the fallback. Returns `None` on registry access failure.
232
233        const SUBKEY_PATH: &str = r"SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\location";
234        let reg_value_paths = [
235            // Local machine.
236            RegValuePath {
237                hkey: HKEY_LOCAL_MACHINE,
238                subkey_path: SUBKEY_PATH,
239                value_name: "Value",
240            },
241            // Current user apps.
242            RegValuePath {
243                hkey: HKEY_CURRENT_USER,
244                subkey_path: SUBKEY_PATH,
245                value_name: "Value",
246            },
247            // Current user desktop apps.
248            RegValuePath {
249                hkey: HKEY_CURRENT_USER,
250                subkey_path: &format!(r"{SUBKEY_PATH}\NonPackaged"),
251                value_name: "Value",
252            },
253        ];
254
255        for RegValuePath {
256            hkey,
257            subkey_path,
258            value_name,
259        } in reg_value_paths
260        {
261            if RegKey::predef(hkey)
262                .open_subkey_with_flags(subkey_path, KEY_QUERY_VALUE)
263                .ok()?
264                .get_value::<String, _>(value_name)
265                .ok()?
266                != "Allow"
267            {
268                return Some(false);
269            }
270        }
271
272        Some(true)
273    }
274
275    pub fn active(&self) -> bool {
276        //! Whether night time color temperature is currently in effect, be it because manually chosen or by schedule.
277
278        *self.state.active
279    }
280
281    pub fn set_active(&mut self, active: bool) {
282        self.state.active.set(active);
283    }
284
285    pub fn transition_cause(&self) -> TransitionCause {
286        self.state.transition_cause
287    }
288
289    pub fn state_modified_filetime(&self) -> i64 {
290        //! The state registry value's modification timestamp as a [`FILETIME`](https://learn.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-filetime) (always greater than 0).
291
292        self.state.modified_filetime
293    }
294
295    pub fn latest_possible_settings_modified_epoch_secs(&self) -> u32 {
296        //! The settings registry value's modification timestamp, or a value somewhat later when many changes were performed in quick succession (e.g., by moving the slider in the official settings).
297
298        self.settings.prologue_epoch_secs
299    }
300
301    pub fn schedule_active(&self) -> bool {
302        *self.settings.schedule_active
303    }
304
305    pub fn set_schedule_active(&mut self, schedule_active: bool) {
306        self.settings.schedule_active.set(schedule_active);
307    }
308
309    pub fn schedule_type(&self) -> ScheduleType {
310        //! The theoretical schedule type. The one really in effect also depends on the state of location services. See also other getter.
311
312        *self.settings.schedule_type
313    }
314
315    pub fn effective_schedule_type(&self) -> Option<ScheduleType> {
316        //! The schedule type really in effect, which also depends on location services being turned on or off. It's unknown how Windows behaves when location services are turned on, but the location still can't be retrieved for a longer period of time. Returns `None` on schedule type "Sunset to sunrise", if [`Self::sunset_to_sunrise_possible()`] did.
317
318        match *self.settings.schedule_type {
319            ScheduleType::SunsetToSunrise => {
320                if self.sunset_to_sunrise_possible? {
321                    Some(ScheduleType::SunsetToSunrise)
322                } else {
323                    Some(ScheduleType::Explicit)
324                }
325            }
326            ScheduleType::Explicit => Some(ScheduleType::Explicit),
327        }
328    }
329
330    pub fn set_schedule_type(&mut self, schedule_type: ScheduleType) {
331        //! Because the explicit schedule is the fallback of "Sunset to sunrise", there won't be a change of the effective schedule type in the official Night Light settings when location services are off. The change is still perfomed however - just that it can only become effective when location services are turned on.
332
333        self.settings.schedule_type.set(schedule_type);
334    }
335
336    pub fn sunset_to_sunrise(&self) -> Option<ClockTimeFrame> {
337        //! Returns `None`, if all of the respective hour and minute values in the registry value were zero.
338
339        self.settings.sunset_to_sunrise
340    }
341
342    pub fn scheduled_night(&self) -> ClockTimeFrame {
343        //! The clock times defining the explicit schedule.
344
345        *self.settings.scheduled_night
346    }
347
348    pub fn set_scheduled_night(&mut self, scheduled_night: ClockTimeFrame) {
349        //! You can use *any* combination of clock times precise to the minute, and it will be adhered to. The time pickers in the official Night Light settings, however, will just display the times with 15-minute accuracy, rounded down. Two times the same clock time means zero-length night.
350
351        self.settings.scheduled_night.set(scheduled_night);
352    }
353
354    pub fn night_color_temp(&self) -> Option<u16> {
355        //! The night time color temperature in Kelvin. May possibly be out of the range of the constants, if Microsoft changed them. Returns `None`, if the information wasn't present in the registry value, in which case Windows applies the default.
356
357        *self.settings.night_color_temp
358    }
359
360    pub fn night_color_temp_in_range(&self) -> Option<u16> {
361        //! The color temperature guaranteed to be in the range of the constants.
362
363        self.settings
364            .night_color_temp
365            .map(|temp| temp.clamp(Self::MIN_NIGHT_COLOR_TEMP, Self::MAX_NIGHT_COLOR_TEMP))
366    }
367
368    pub fn set_night_color_temp(&mut self, night_color_temp: Option<u16>) {
369        //! Windows corrects the value to lie in the valid range. `None` makes Windows apply the default.
370
371        self.settings.night_color_temp.set(night_color_temp);
372    }
373
374    pub fn warmth(&self) -> Option<f32> {
375        //! A factor in the range from 0 to 1, based on the color temperature range constants. May return `None` like the color temperature getter. Corresponds to the "Strength" slider in the official Night Light settings, which shows a percentage.
376
377        self.night_color_temp_in_range().map(|temp| {
378            1.0 - (temp - Self::MIN_NIGHT_COLOR_TEMP) as f32
379                / (Self::MAX_NIGHT_COLOR_TEMP - Self::MIN_NIGHT_COLOR_TEMP) as f32
380        })
381    }
382
383    pub fn set_warmth(&mut self, warmth: Option<f32>) {
384        //! Steps in the upper range are perceived as more intense, which is why they should be smaller to achieve the same step in perception as larger steps in the lower range. There isn't a one-size-fits-all correction curve, since color temperature perception also depends on light intensity (see <https://en.wikipedia.org/wiki/Kruithof_curve>). But gamma correction (exponentiation) still seems suitable.
385        //!
386        //! # Panics
387        //! Panics on NaN.
388
389        self.set_night_color_temp(warmth.map(|warmth| {
390            if warmth.is_nan() {
391                panic!("value is NaN");
392            }
393
394            let precise_temp = (Self::MAX_NIGHT_COLOR_TEMP - Self::MIN_NIGHT_COLOR_TEMP) as f32
395                * (1.0 - warmth)
396                + Self::MIN_NIGHT_COLOR_TEMP as f32;
397            precise_temp.round().clamp(0f32, u16::MAX as f32) as u16
398        }));
399    }
400
401    pub fn night_preview_active(&self) -> bool {
402        //! Whether preview mode with a hard change (as opposed to a smooth transition) to night color temperature is in effect. The official Night Light settings activate this while moving the color temperature slider.
403
404        *self.settings.night_preview_active
405    }
406
407    pub fn set_night_preview_active(&mut self, night_preview_active: bool) {
408        self.settings.night_preview_active.set(night_preview_active);
409    }
410
411    pub fn set_uses_12_hour_clock(&mut self, uses_12_hour_clock: bool) {
412        //! Only for display purposes.
413
414        self.uses_12_hour_clock = uses_12_hour_clock;
415    }
416
417    pub fn to_json(&self) -> String {
418        serde_json::to_string_pretty(&json!({
419            "active": *self.state.active,
420            "transitionCause": format!("{:?}", self.state.transition_cause).to_case(Case::Camel),
421            "stateModifiedTimestamp": utc_filetime_to_local_iso_string(self.state.modified_filetime).expect("`FILETIME` should be valid"),
422
423            "latestPossibleSettingsModifiedTimestamp": utc_epoch_secs_to_local_iso_string(self.settings.prologue_epoch_secs).expect("epoch secs should be valid"),
424            "scheduleActive": *self.settings.schedule_active,
425            "scheduleType": format!("{:?}", *self.settings.schedule_type).to_case(Case::Camel),
426            "sunsetToSunrisePossible": self.sunset_to_sunrise_possible,
427            "effectiveScheduleType": self.effective_schedule_type().map(|r#type| format!("{:?}", r#type).to_case(Case::Camel)),
428            "sunsetToSunrise": self.settings.sunset_to_sunrise,
429            "scheduledNight": *self.settings.scheduled_night,
430            "nightColorTemp": *self.settings.night_color_temp,
431            "warmth": self.warmth(),
432            "nightPreviewActive": *self.settings.night_preview_active,
433        }))
434        .expect("serializing to JSON shouldn't fail")
435    }
436
437    pub fn write_to_reg(mut self) -> Result<(), self::Error> {
438        //! Writes the data to the registry values, which immediately applies it.
439
440        if self.loaded_instant.elapsed() > Self::EXPIRATION_TIMEOUT {
441            return Err(DataError::Expired.into());
442        }
443
444        let (state_changed, settings_changed) = self.verify_state_and_settings()?;
445
446        let state_bytes = state_changed.then(|| {
447            //. Only Windows is allowed to write the other value, because it does so by schedule.
448            self.state.transition_cause = TransitionCause::Manual;
449
450            self.state.to_bytes()
451        });
452        let settings_bytes = settings_changed.then(|| self.settings.to_bytes());
453
454        // Write settings first, then state.
455        if let Some(settings_bytes) = settings_bytes {
456            write_reg_bin_value(&RawNightLightSettings::REG_VALUE_PATH, &settings_bytes)?;
457            // (When state-changing settings were changed, Windows may now change the state registry value.)
458        }
459        if let Some(state_bytes) = state_bytes {
460            write_reg_bin_value(&RawNightLightState::REG_VALUE_PATH, &state_bytes)?;
461        }
462
463        Ok(())
464    }
465
466    fn verify_state_and_settings(&mut self) -> Result<(bool, bool), DataError> {
467        //! Returns whether the state and the settings were changed.
468
469        let active_changed = self.state.active.changed();
470
471        let schedule_active_changed = self.settings.schedule_active.changed();
472        let schedule_type_changed = self.settings.schedule_type.changed();
473        let scheduled_night_changed = self.settings.scheduled_night.changed();
474        let night_color_temp_changed = self.settings.night_color_temp.changed();
475        let night_preview_active_changed = self.settings.night_preview_active.changed();
476
477        let state_changed = active_changed;
478        let settings_changed = schedule_active_changed
479            || schedule_type_changed
480            || scheduled_night_changed
481            || night_color_temp_changed
482            || night_preview_active_changed;
483        let state_changing_settings_changed =
484            schedule_active_changed || schedule_type_changed || scheduled_night_changed;
485
486        if state_changed && state_changing_settings_changed {
487            return Err(DataError::Irreconcilable(
488                CompetingProps::StateVsStateChangingSettings,
489            ));
490        }
491
492        if (*self.settings.night_preview_active /*turned on or held active*/ || night_preview_active_changed/*turned off*/)
493            && (state_changed || state_changing_settings_changed)
494        {
495            return Err(if night_preview_active_changed {
496                DataError::Irreconcilable(if state_changed {
497                    CompetingProps::StateVsNightPreview
498                } else {
499                    CompetingProps::StateChangingSettingsVsNightPreview
500                })
501            } else {
502                // Unchanged, but active night preview. Interfering with other software in this state makes adverse effects likely.
503                DataError::NightPreviewInProgress
504            });
505        }
506
507        Ok((state_changed, settings_changed))
508    }
509}
510
511impl fmt::Display for NightLight {
512    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
513        let bool_to_yes_no = |flag| if flag { "yes" } else { "no" }.to_string();
514        let parenthesize_if = |flag, string| if flag { format!("({string})") } else { string };
515
516        let effective_schedule_type = self.effective_schedule_type();
517
518        write_table(
519            f,
520            &[
521                Some(("Active", bool_to_yes_no(*self.state.active))),
522                Some((
523                    "Transition Cause",
524                    format!("{:?}", self.state.transition_cause).to_case(Case::Lower),
525                )),
526                None,
527                Some((
528                    "Warmth",
529                    self.warmth()
530                        .map(|warmth| format!("{warmth:.2}"))
531                        .unwrap_or_else(|| format!("default (should be {})", Self::DEFAULT_WARMTH)),
532                )),
533                Some((
534                    "Kelvin",
535                    self.settings
536                        .night_color_temp
537                        .map(|temp| temp.to_string())
538                        .unwrap_or_else(|| {
539                            format!("default (should be {})", Self::DEFAULT_NIGHT_COLOR_TEMP)
540                        }),
541                )),
542                Some((
543                    "Preview Active",
544                    bool_to_yes_no(*self.settings.night_preview_active),
545                )),
546                None,
547                Some((
548                    "Schedule Active",
549                    bool_to_yes_no(*self.settings.schedule_active),
550                )),
551                Some((
552                    "Schedule Type (Effective)",
553                    effective_schedule_type
554                        .map(|r#type| format!("{:?}", r#type).to_case(Case::Lower))
555                        .unwrap_or_else(|| "N/A".to_string()),
556                )),
557                Some((
558                    "Sunset to Sunrise",
559                    parenthesize_if(
560                        effective_schedule_type == Some(ScheduleType::Explicit),
561                        self.settings
562                            .sunset_to_sunrise
563                            .map(|frame| frame.format(self.uses_12_hour_clock))
564                            .unwrap_or_else(|| "N/A".to_string()),
565                    ),
566                )),
567                Some((
568                    "Explicit Night",
569                    parenthesize_if(
570                        effective_schedule_type == Some(ScheduleType::SunsetToSunrise),
571                        self.settings
572                            .scheduled_night
573                            .format(self.uses_12_hour_clock),
574                    ),
575                )),
576                None,
577                Some((
578                    "Modified (Latest Possible)",
579                    utc_filetime_to_local_date_time(self.state.modified_filetime.max(
580                        epoch_duration_to_filetime(Duration::from_secs(
581                            self.settings.prologue_epoch_secs as _,
582                        )),
583                    ))
584                    .ok_or(fmt::Error)?
585                    .format(if self.uses_12_hour_clock {
586                        "%Y-%m-%d, %I:%M:%S %P"
587                    } else {
588                        "%Y-%m-%d, %H:%M:%S"
589                    })
590                    .to_string(),
591                )),
592            ],
593        )?;
594
595        Ok(())
596    }
597}
598
599impl fmt::Debug for NightLight {
600    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
601        write_table(
602            f,
603            &[
604                Some((
605                    "prologue timestamp (state)",
606                    utc_epoch_secs_to_local_iso_string(self.state.prologue_epoch_secs)
607                        .ok_or(fmt::Error)?,
608                )),
609                Some(("active (state)", self.state.active.to_string())),
610                Some((
611                    "transition cause (state)",
612                    format!("{:?}", self.state.transition_cause),
613                )),
614                Some((
615                    "modified-`FILETIME` (state)",
616                    utc_filetime_to_local_iso_string(self.state.modified_filetime)
617                        .ok_or(fmt::Error)?,
618                )),
619                None,
620                Some((
621                    "prologue timestamp (settings)",
622                    utc_epoch_secs_to_local_iso_string(self.settings.prologue_epoch_secs)
623                        .ok_or(fmt::Error)?,
624                )),
625                Some((
626                    "schedule active (settings)",
627                    self.settings.schedule_active.to_string(),
628                )),
629                Some((
630                    "schedule type (settings)",
631                    format!("{:?}", *self.settings.schedule_type),
632                )),
633                Some((
634                    "sunset-to-sunrise possible (other)",
635                    format!("{:?}", self.sunset_to_sunrise_possible),
636                )),
637                Some((
638                    "effective schedule type (settings & other)",
639                    format!("{:?}", self.effective_schedule_type()),
640                )),
641                Some((
642                    "sunset to sunrise (settings)",
643                    self.settings
644                        .sunset_to_sunrise
645                        .map(|frame| frame.format(self.uses_12_hour_clock))
646                        .unwrap_or_else(|| format!("{:?}", None::<()>)),
647                )),
648                Some((
649                    "scheduled night (settings)",
650                    self.settings
651                        .scheduled_night
652                        .format(self.uses_12_hour_clock),
653                )),
654                Some((
655                    "night color temp. (settings)",
656                    format!("{:?}", *self.settings.night_color_temp),
657                )),
658                Some((
659                    "warmth (settings, processed)",
660                    format!("{:?}", self.warmth()),
661                )),
662                Some((
663                    "night preview active (settings)",
664                    self.settings.night_preview_active.to_string(),
665                )),
666                None,
667                Some((
668                    "loaded-`Instant`",
669                    chrono::Local::now()
670                        .sub(self.loaded_instant.elapsed())
671                        .to_rfc3339_opts(SecondsFormat::Millis, true),
672                )),
673                Some(("strictness", format!("{:?}", self.strictness))),
674            ],
675        )?;
676
677        Ok(())
678    }
679}
680
681#[derive(thiserror::Error, Debug)]
682pub enum Error {
683    /// Error interacting with the registry, e.g., because of non-existent registry value. If the user never used Night Light in their OS installation, they should be advised to change something in the official settings to create the registry values and try again (turning Night Light on/off and moving the temperature slider should suffice).
684    #[error("IO error: {0}")]
685    IoError(#[from] io::Error),
686    /// Couldn't parse a byte stream.
687    #[error("parse error: {0}")]
688    ParseError(#[from] ParseError),
689    /// Couldn't serialize the data from an instance into a byte stream.
690    #[error("data error: {0}")]
691    DataError(#[from] DataError),
692}
693
694#[derive(thiserror::Error, Debug)]
695pub enum DataError {
696    /// The object expired to enforce avoidance of race conditions.
697    #[error("object expired: duration between reading and writing was too long")]
698    Expired,
699    /// Changed properties don't harmonize with the condition of other properties.
700    #[error("changed props are irreconcilable with other props: {0}")]
701    Irreconcilable(CompetingProps),
702    /// Night preview is currently active, because of other software or earlier use of this crate. The user could, e.g., right now be dragging the color tempature slider in the official Night Light settings.
703    #[error("night preview was active while trying to change props irreconcilable with it")]
704    NightPreviewInProgress,
705}
706
707#[derive(Clone, Debug)]
708pub struct NightLightBytes {
709    pub state: Vec<u8>,
710    pub settings: Vec<u8>,
711}
712
713impl NightLightBytes {
714    pub fn from_reg() -> Result<Self, io::Error> {
715        Ok(Self {
716            state: read_reg_bin_value(&RawNightLightState::REG_VALUE_PATH)?,
717            settings: read_reg_bin_value(&RawNightLightSettings::REG_VALUE_PATH)?,
718        })
719    }
720
721    pub fn bytes_of_value(&self, reg_value_id: RegValueId) -> &[u8] {
722        match reg_value_id {
723            RegValueId::State => &*self.state,
724            RegValueId::Settings => &*self.settings,
725        }
726    }
727}
728
729#[derive(Debug)]
730pub enum CompetingProps {
731    StateVsStateChangingSettings,
732    StateVsNightPreview,
733    StateChangingSettingsVsNightPreview,
734}
735
736impl fmt::Display for CompetingProps {
737    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
738        match self {
739            CompetingProps::StateVsStateChangingSettings => {
740                write!(f, "state vs. state-changing settings")
741            }
742            CompetingProps::StateVsNightPreview => write!(f, "state vs. night preview"),
743            CompetingProps::StateChangingSettingsVsNightPreview => {
744                write!(f, "state-changing settings vs. night preview")
745            }
746        }
747    }
748}
749
750#[derive(Clone, Copy, Debug)]
751pub enum RegValueId {
752    State,
753    Settings,
754}
755
756#[cfg(test)]
757mod tests {
758    use crate::cloud_store::night_light::NightLight;
759
760    #[ignore]
761    #[test]
762    fn playground() -> Result<(), super::Error> {
763        let mut night_light = NightLight::from_reg()?;
764
765        // night_light.set_active(true);
766
767        night_light.set_schedule_active(true);
768        // night_light.set_schedule_type(ScheduleType::Explicit);
769        // night_light.set_scheduled_night(ClockTimeFrame {
770        //     start: ClockTime::from_h_min(23, 6)?,
771        //     end: ClockTime::from_h_min(6, 42)?,
772        // });
773
774        // night_light.set_night_color_temp(2000);
775        // night_light.set_warmth(0.7);
776
777        // night_light.set_night_preview_active(true);
778
779        night_light.write_to_reg()?;
780
781        Ok(())
782    }
783
784    #[test]
785    fn from_reg_strict() {
786        let result = NightLight::from_reg();
787        assert!(result.is_ok(), "{result:?}");
788    }
789
790    #[test]
791    fn from_reg_lenient() {
792        let result = NightLight::from_reg_lenient();
793        assert!(result.is_ok(), "{result:?}");
794    }
795
796    #[test]
797    fn compare_strict_with_lenient_reg_result() {
798        let strict_result = NightLight::from_reg();
799        let lenient_result = NightLight::from_reg_lenient();
800
801        if let (Ok(strict), Ok(lenient)) = (strict_result, lenient_result) {
802            assert!(strict.state == lenient.state, "unequal states");
803            assert!(strict.settings == lenient.settings, "unequal settings");
804        } else {
805            panic!("no `NightLight`");
806        }
807    }
808
809    #[test]
810    fn sunset_to_sunrise_possible_is_some() {
811        assert!(NightLight::sunset_to_sunrise_possible().is_some());
812    }
813
814    #[test]
815    fn verify_warmth_setter() -> Result<(), super::Error> {
816        let mut night_light = NightLight::from_reg()?;
817
818        assert_eq!(NightLight::WARMEST_NIGHT_COLOR_TEMP, 1200);
819        assert_eq!(NightLight::COLDEST_NIGHT_COLOR_TEMP, 6500);
820
821        night_light.set_warmth(Some(0.7));
822        assert_eq!(night_light.night_color_temp(), Some(2790));
823
824        Ok(())
825    }
826}