//! Pure theme resolution, appearance scheduling, and readable transition samples. use std::path::{Path, PathBuf}; use jiff::civil::{DateTime, Time}; use jiff::tz::{Disambiguation, TimeZone}; use jiff::{Timestamp, ToSpan}; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; use crate::Document; pub const SCHEMA_VERSION: u32 = 2; pub const TRANSITION_MS: u64 = 250; #[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum Appearance { Light, #[default] Dark, } #[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum Mode { Light, #[default] Dark, Auto, } #[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] #[serde(default)] pub struct Accessibility { pub increase_contrast: bool, pub reduce_transparency: bool, } #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] #[serde(default)] pub struct Selection { pub family: Option, pub preset: String, pub file: Option, #[serde(flatten)] pub overrides: serde_json::Map, } impl Default for Selection { fn default() -> Self { Self { preset: "ferese-blue".into(), family: None, file: None, overrides: Default::default(), } } } #[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub enum AutoSource { System, SunriseSunset, #[default] Schedule, } #[derive(Clone, Debug, Default)] pub struct AutoContext { pub system: Option, } #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] #[serde(default)] pub struct Schedule { pub source: AutoSource, pub timezone: String, pub light_at: String, pub dark_at: String, } impl Default for Schedule { fn default() -> Self { Self { source: AutoSource::Schedule, timezone: "system".into(), light_at: "07:00".into(), dark_at: "19:00".into(), } } } impl Schedule { pub fn resolve( &self, now: Timestamp, zone: &TimeZone, context: &AutoContext, ) -> Result<(Appearance, Option, Option), String> { match self.source { AutoSource::System => Ok(( context.system.unwrap_or(Appearance::Dark), None, context .system .is_none() .then(|| "System appearance is unavailable. Using Dark until it becomes available.".into()), )), AutoSource::Schedule => self .boundaries(now, zone) .map(|(appearance, next)| (appearance, Some(next), None)), AutoSource::SunriseSunset => { Err("Sunrise/sunset is not available yet; choose Follow system or Custom schedule.".into()) } } } pub fn zone(&self) -> Result { if self.timezone == "system" { if std::env::var_os("TZ").is_some() { TimeZone::try_system().map_err(|e| e.to_string()) } else { let bytes = std::fs::read("/etc/localtime").map_err(|e| e.to_string())?; TimeZone::tzif("system", &bytes).map_err(|e| e.to_string()) } } else { TimeZone::get(&self.timezone).map_err(|e| e.to_string()) } } pub fn boundaries(&self, now: Timestamp, zone: &TimeZone) -> Result<(Appearance, Timestamp), String> { let parse = |s: &str| -> Result { if s.len() != 5 || s.as_bytes()[2] != b':' { return Err("theme schedule times must use HH:MM".into()); } format!("{s}:00").parse::