hinoirisetr 1.5.5

A daemon to dim the screen at night
Documentation
use std::process::Command;
use std::sync::atomic::{AtomicU16, Ordering};
use std::sync::{Arc, LazyLock, Mutex};

use rayon::prelude::*;

#[cfg(feature = "wayland-backend")]
use crate::backend::WAYLAND_THREAD;
use crate::utils::eq_ignore_case;
use crate::{debug, ensure_hyprsunset_running, error, trace, warn};

static LAST_GAMMA: AtomicU16 = AtomicU16::new(0);
static GAMMA_LOCK: LazyLock<Arc<Mutex<()>>> = LazyLock::new(|| Arc::new(Mutex::new(())));

#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum GammaBackend {
    Hyprctl,
    Ddcutil,
    Xsct,
    ACPI,
    #[cfg(feature = "wayland-backend")]
    Wayland,
    None,
}

/// it is case insensitive
impl std::str::FromStr for GammaBackend {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            s if eq_ignore_case(s, "hyprctl") => {
                warn!(
                    "hyprctl backend has been superseded by wayland, it may be removed in the next release"
                );
                Ok(Self::Hyprctl)
            },
            s if eq_ignore_case(s, "ddcutil") => Ok(Self::Ddcutil),
            s if eq_ignore_case(s, "gammastep") => Err(
                "gammastep backend has been deprecated, use wayland or xset instead".to_string(),
            ),
            s if eq_ignore_case(s, "xsct") => Ok(Self::Xsct),
            s if eq_ignore_case(s, "redshift") => {
                Err("redshift backend has been deprecated, use wayland or xset instead".to_string())
            },
            s if eq_ignore_case(s, "acpi") => Ok(Self::ACPI),
            #[cfg(feature = "wayland-backend")]
            s if eq_ignore_case(s, "wayland") => Ok(Self::Wayland),
            #[cfg(not(feature = "wayland-backend"))]
            s if eq_ignore_case(s, "wayland") => {
                Err(format!("hinoirisetr was compiled without wayland support"))
            },
            s if eq_ignore_case(s, "none") => Ok(Self::None),
            _ => Err(format!("Invalid GammaBackend: {s}")),
        }
    }
}

/// Apply given gamma (percentage) via given backends
pub fn apply_gamma(gamma: u16, backends: &[GammaBackend]) {
    trace!("apply_gamma({gamma})");
    let _lock = GAMMA_LOCK.lock().unwrap();
    let last_gamma = LAST_GAMMA.load(Ordering::SeqCst);
    if last_gamma == gamma {
        trace!("Settings unchanged, skipping application");
        return;
    }
    debug!("applying gamma: {gamma}");

    for backend in backends {
        match backend {
            GammaBackend::Hyprctl => match ensure_hyprsunset_running() {
                Ok(()) => {
                    let _ = Command::new("hyprctl")
                        .args(["hyprsunset", "gamma", &gamma.to_string()])
                        .output();
                    trace!("hyprctl hyprsunset gamma {gamma}");
                },
                Err(err) => {
                    error!("Error while starting hyprsunset: {err}");
                },
            },
            GammaBackend::Ddcutil => {
                let display_count = match Command::new("ddcutil").args(["detect"]).output() {
                    Ok(output) => String::from_utf8_lossy(&output.stdout)
                        .trim()
                        .to_string()
                        .lines()
                        .filter(|l| l.contains("Display"))
                        .count(),
                    Err(e) => {
                        error!("Failed to detect the number of displays with error: {e}");
                        1
                    },
                };

                (1..=display_count).into_par_iter().for_each(|display| {
                    let _ = Command::new("ddcutil")
                        .args([
                            "-d",
                            &display.to_string(),
                            "setvcp",
                            "10",
                            &gamma.to_string(),
                        ])
                        .output();
                    trace!("ddcutil -d {display} setvcp 10 {gamma}");
                });
            },
            GammaBackend::Xsct => {
                let output = Command::new("xsct").output().expect("xsct failed");
                let stdout = String::from_utf8_lossy(&output.stdout);
                let mut ttemp = 6000;
                for line in stdout.lines() {
                    if line.contains("temperature") {
                        // example: "Screen 0: temperature ~ 6000 0.598234"
                        let parts: Vec<&str> = line.split_whitespace().collect();
                        if parts.len() >= 6 {
                            ttemp = parts[4].parse::<u16>().unwrap();
                        }
                    }
                }

                let _ = Command::new("xsct")
                    .args([&ttemp.to_string(), &(&gamma / 100).to_string()])
                    .output();
                trace!("xsct {ttemp} {gamma}");
            },
            GammaBackend::ACPI => {
                if let Ok(dir) = std::fs::read_dir("/sys/class/backlight") {
                    for file in dir.flatten() {
                        let mut path = file.path();
                        if path.is_dir() {
                            path.push("max_brightness");

                            match std::fs::read_to_string(&path) {
                                Ok(max_brightness_str) => {
                                    match max_brightness_str.trim().parse::<u16>() {
                                        Ok(max_brightness) => {
                                            let brightness = ((f64::from(gamma) / 100.0)
                                                * f64::from(max_brightness))
                                                as u16;
                                            path.pop();
                                            path.push("brightness");
                                            if let Err(e) =
                                                std::fs::write(&path, format!("{brightness}"))
                                            {
                                                error!(
                                                    "Failed to write brightness to {}; error: {}",
                                                    path.display(),
                                                    e
                                                );
                                            }
                                        },
                                        Err(e) => {
                                            error!(
                                                "Failed to parse max_brightness from {}; error: {}",
                                                path.display(),
                                                e
                                            );
                                        },
                                    }
                                },
                                Err(e) => {
                                    error!("Failed to read {}; error: {}", path.display(), e);
                                },
                            }
                        }
                    }
                } else {
                    error!(
                        "Failed to read ACPI backend directory /sys/class/backlight; check your permissions or ensure the directory exists"
                    );
                }
            },
            #[cfg(feature = "wayland-backend")]
            GammaBackend::Wayland => {
                let guard = WAYLAND_THREAD.read().unwrap();
                if let Some(ref thread) = *guard {
                    thread.set_gamma(gamma as u8);
                }
            },
            GammaBackend::None => {},
        }
    }
    LAST_GAMMA.store(gamma, Ordering::SeqCst);
}

pub fn reset_cache() {
    LAST_GAMMA.store(0, Ordering::SeqCst);
}