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,
}
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("hinoirisetr was compiled without wayland support".to_string())
},
s if eq_ignore_case(s, "none") => Ok(Self::None),
_ => Err(format!("Invalid GammaBackend: {s}")),
}
}
}
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") {
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);
}