float-clock-wayland 0.2.0

Always-on-top floating desktop clock widget for Wayland compositors.
use chrono::{Local, NaiveDate};

pub struct Clock {
    demo: bool,
}

impl Clock {
    pub fn new(demo: bool) -> Self {
        Self { demo }
    }

    /// Format current time or demo time based on chrono format string.
    pub fn now_formatted(&self, format_str: &str) -> String {
        let fmt = format_str.replace("\\n", "\n");
        if self.demo {
            let demo_dt = NaiveDate::from_ymd_opt(2007, 1, 9)
                .unwrap()
                .and_hms_opt(9, 41, 0)
                .unwrap();
            demo_dt.format(&fmt).to_string()
        } else {
            Local::now().format(&fmt).to_string()
        }
    }

    /// High-precision calculation of milliseconds until the next clock update is required.
    /// Aligns dynamically to wall-clock second tick boundaries with a 2ms guard offset.
    pub fn millis_until_next_tick(&self, format_str: &str) -> u64 {
        if self.demo {
            return 3600_000; // Static demo mode needs no frequent ticks
        }

        // Sub-second formats require high frequency polling (e.g. 30 Hz ~ 33ms)
        if format_str.contains("%f")
            || format_str.contains("%3f")
            || format_str.contains("%6f")
            || format_str.contains("%9f")
            || format_str.contains("%N")
        {
            return 33;
        }

        // Phase-locked alignment to second boundaries:
        let now = Local::now();
        let millis = now.timestamp_subsec_millis();
        let delay = 1000 - (millis % 1000) + 2; // +2ms guard offset to ensure we step past the second mark
        u64::from(delay)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_demo_clock_formatting() {
        let clock = Clock::new(true);
        let formatted = clock.now_formatted("%Y-%m-%d %H:%M:%S");
        assert_eq!(formatted, "2007-01-09 09:41:00");
    }

    #[test]
    fn test_millis_until_next_tick_bounds() {
        let clock = Clock::new(false);
        let delay = clock.millis_until_next_tick("%H:%M:%S");
        assert!(delay >= 2 && delay <= 1002, "Delay {} out of expected range", delay);
    }

    #[test]
    fn test_subsecond_format_detection() {
        let clock = Clock::new(false);
        let delay = clock.millis_until_next_tick("%H:%M:%S.%3f");
        assert_eq!(delay, 33);
    }
}