hinoirisetr 0.1.0

A daemon to dim the screen at night
Documentation
//! hinoirisetr library
//! Contains core logic for computing temperature and gamma and applying settings.

use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::Path;
use std::process::Command;

use time::Time;

pub mod log;
pub mod time;
pub mod notify;

#[derive(Debug, Copy, Clone)]
pub struct Config {
    pub temp_day: u16,
    pub temp_night: u16,

    pub gamma_day: u16,
    pub gamma_night: u16,

    pub sunset_start: u8,
    pub sunset_end: u8,
    pub sunrise_start: u8,
    pub sunrise_end: u8,

    pub notification_timeout: u32,
}

impl Default for Config {
    fn default() -> Self {
        Self {
            temp_day: 6500,
            temp_night: 2500,
            gamma_day: 100,
            gamma_night: 95,
            sunset_start: 19,
            sunset_end: 22,
            sunrise_start: 4,
            sunrise_end: 7,
            notification_timeout: 5000,
        }
    }
}

impl Config {
    pub fn load<P: AsRef<Path> + std::fmt::Debug>(
        path: P,
    ) -> Result<Self, Box<dyn std::error::Error>> {
        trace!("Config::load({path:?})");
        let mut config = Self::default(); // Start with default values

        let config_file = File::open(path)?;
        let reader = BufReader::new(config_file);
        let mut current_section = String::new();

        for line in reader
            .lines()
            .map_while(Result::ok)
            .map(|l| String::from(l.trim()))
            .filter(|l| !l.is_empty())
            .filter(|l| !l.starts_with('#'))
        {
            trace!("line: {line}");
            if line.starts_with('[') && line.contains(']') {
                current_section = line[1..line.find(']').unwrap()].to_string();
                trace!("current_section: {current_section}");
            } else if let Some((key, value)) = line.split_once('=') {
                trace!("key: {key}, value: {value}");
                let key_trimmed_string = key.trim().replace('"', "");
                let key_trimmed = key_trimmed_string.as_str();

                let value = value.trim().replace('"', "");
                match current_section.as_str() {
                    "" => {
                        if key_trimmed == "notification_timeout" {
                            config.notification_timeout = value.parse()?;
                        }
                    }
                    "gamma" => match key_trimmed {
                        "day" => config.gamma_day = value.parse()?,
                        "night" => config.gamma_night = value.parse()?,
                        _ => {}
                    },
                    "temp" => match key_trimmed {
                        "day" => config.temp_day = value.parse()?,
                        "night" => config.temp_night = value.parse()?,
                        _ => {}
                    },
                    "time" => match key_trimmed {
                        "sunset_start" => config.sunset_start = value.parse()?,
                        "sunset_end" => config.sunset_end = value.parse()?,
                        "sunrise_start" => config.sunrise_start = value.parse()?,
                        "sunrise_end" => config.sunrise_end = value.parse()?,
                        _ => {}
                    },
                    _ => {}
                }
            }
        }

        Ok(config)
    }
}

/// Linearly interpolate between start and end by factor [0.0, 1.0]
pub fn interpolate(start: u16, end: u16, factor: f64) -> u16 {
    trace!("interpolate({start}, {end}, {factor})");
    if end < start {
        (end as f64 + (start - end) as f64 * (1.0 - factor)).round() as u16
    } else {
        (start as f64 + (end - start) as f64 * factor).round() as u16
    }
}

/// Compute current temperature and gamma based on provided time
pub fn compute_settings(now: Time, config: &Config) -> (u16, u16) {
    trace!("compute_settings({now:?})");
    let time_in_hours = now.hour() as f64 + now.minute() as f64 / 60.0;
    trace!("time_in_hours: {time_in_hours}");

    if (time_in_hours >= config.sunset_start as f64) && (time_in_hours <= config.sunset_end as f64)
    {
        trace!("time_in_hours is within sunset");
        let factor = ((time_in_hours - config.sunset_start as f64)
            / (config.sunset_end - config.sunset_start) as f64)
            .clamp(0.0, 1.0);
        (
            interpolate(config.temp_day, config.temp_night, factor),
            interpolate(config.gamma_day, config.gamma_night, factor),
        )
    } else if (time_in_hours >= config.sunrise_start as f64)
        && (time_in_hours <= config.sunrise_end as f64)
    {
        trace!("time_in_hours is within sunrise");
        let factor = 1.0
            - ((time_in_hours - config.sunrise_start as f64)
                / (config.sunrise_end - config.sunrise_start) as f64)
                .clamp(0.0, 1.0);
        (
            interpolate(config.temp_day, config.temp_night, factor),
            interpolate(config.gamma_day, config.gamma_night, factor),
        )
    } else if time_in_hours > config.sunset_end as f64
        || time_in_hours < config.sunrise_start as f64
    {
        trace!("time_in_hours is within night");
        (config.temp_night, config.gamma_night)
    } else {
        trace!("time_in_hours is within day");
        (config.temp_day, config.gamma_day)
    }
}

/// Apply given temperature (Kelvin) and gamma (%) via hyprctl commands
pub fn apply_settings(temp: u16, gamma: u16) {
    trace!("apply_settings({temp}, {gamma})");
    debug!("applying temperature: {temp}");
    debug!("applying gamma: {gamma}");

    let _ = Command::new("hyprctl")
        .args(["hyprsunset", "temperature", &temp.to_string()])
        .output();
    trace!("hyprctl hyprsunset temperature {temp}");

    let _ = Command::new("hyprctl")
        .args(["hyprsunset", "gamma", &gamma.to_string()])
        .output();
    trace!("hyprctl hyprsunset gamma {gamma}");
}