hinoirisetr/src/lib.rs

386 lines
14 KiB
Rust

//! 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 std::sync::atomic::{AtomicU16, Ordering};
use time::Time;
pub mod log;
pub mod notify;
pub mod time;
#[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,
pub gamma_backend: GammaBackend,
pub temp_backend: TempBackend,
}
static LAST_TEMP: AtomicU16 = AtomicU16::new(0);
static LAST_GAMMA: AtomicU16 = AtomicU16::new(0);
#[derive(Debug, PartialEq, Copy, Clone)]
pub enum GammaBackend {
Hyprctl,
Ddcutil,
Xsct,
Redshift,
Gammastep,
None,
}
#[derive(Debug, PartialEq, Copy, Clone)]
pub enum TempBackend {
Hyprctl,
Redshift,
Xsct,
Gammastep,
None,
}
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,
gamma_backend: GammaBackend::Hyprctl,
temp_backend: TempBackend::Hyprctl,
}
}
}
#[derive(Debug)]
pub enum ConfigError {
IoError(std::io::Error),
ParseIntError(std::num::ParseIntError),
InvalidTemperature(String),
InvalidTime(String),
InvalidGamma(String),
InvalidGammaBackend(String),
InvalidTempBackend(String),
}
impl From<std::io::Error> for ConfigError {
fn from(err: std::io::Error) -> Self {
ConfigError::IoError(err)
}
}
impl From<std::num::ParseIntError> for ConfigError {
fn from(err: std::num::ParseIntError) -> Self {
ConfigError::ParseIntError(err)
}
}
impl Config {
pub fn load<P: AsRef<Path> + std::fmt::Debug>(path: P) -> Result<Self, ConfigError> {
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() {
"" => match key_trimmed {
"notification_timeout" => {
config.notification_timeout = value.parse::<u32>()?;
}
"gamma_backend" => match value.to_lowercase().as_str() {
"hyprctl" => config.gamma_backend = GammaBackend::Hyprctl,
"ddcutil" => config.gamma_backend = GammaBackend::Ddcutil,
"gammastep" => config.gamma_backend = GammaBackend::Gammastep,
"xsct" => config.gamma_backend = GammaBackend::Xsct,
"redshift" => config.gamma_backend = GammaBackend::Redshift,
"none" => config.gamma_backend = GammaBackend::None,
_ => return Err(ConfigError::InvalidGammaBackend(value.to_string())),
},
"temp_backend" => match value.to_lowercase().as_str() {
"hyprctl" => config.temp_backend = TempBackend::Hyprctl,
"gammastep" => config.temp_backend = TempBackend::Gammastep,
"xsct" => config.temp_backend = TempBackend::Xsct,
"redshift" => config.temp_backend = TempBackend::Redshift,
"none" => config.temp_backend = TempBackend::None,
_ => return Err(ConfigError::InvalidTempBackend(value.to_string())),
},
_ => {}
},
"gamma" => match key_trimmed {
"day" => {
let parsed = value.parse::<u16>()?;
if 0 < parsed && parsed <= 100 {
config.gamma_day = parsed
} else {
return Err(ConfigError::InvalidGamma(value.to_string()));
}
}
"night" => {
let parsed = value.parse::<u16>()?;
if 0 < parsed && parsed <= 100 {
config.gamma_night = parsed
} else {
return Err(ConfigError::InvalidGamma(value.to_string()));
}
}
_ => {}
},
"temp" => match key_trimmed {
"day" => {
let parsed = value.parse::<u16>()?;
if (1000..=20000).contains(&parsed) {
config.temp_day = parsed
} else {
return Err(ConfigError::InvalidTemperature(value.to_string()));
}
}
"night" => {
let parsed = value.parse::<u16>()?;
if (1000..=20000).contains(&parsed) {
config.temp_night = parsed
} else {
return Err(ConfigError::InvalidTemperature(value.to_string()));
}
}
_ => {}
},
"time" => match key_trimmed {
"sunset_start" => {
let parsed = value.parse::<u8>()?;
if (0..=23).contains(&parsed) {
config.sunset_start = parsed
} else {
return Err(ConfigError::InvalidTime(value.to_string()));
}
}
"sunset_end" => {
let parsed = value.parse::<u8>()?;
if (0..=23).contains(&parsed) {
config.sunset_end = parsed
} else {
return Err(ConfigError::InvalidTime(value.to_string()));
}
}
"sunrise_start" => {
let parsed = value.parse::<u8>()?;
if (0..=23).contains(&parsed) {
config.sunrise_start = parsed
} else {
return Err(ConfigError::InvalidTime(value.to_string()));
}
}
"sunrise_end" => {
let parsed = value.parse::<u8>()?;
if (0..=23).contains(&parsed) {
config.sunrise_end = parsed
} else {
return Err(ConfigError::InvalidTime(value.to_string()));
}
}
_ => {}
},
_ => {}
}
}
}
if config.sunset_start >= config.sunset_end {
return Err(ConfigError::InvalidTime(format!(
"sunset_start ({0}) is greater than sunset_end ({1})",
config.sunset_start, config.sunset_end
)));
}
if config.sunrise_start >= config.sunrise_end {
return Err(ConfigError::InvalidTime(format!(
"sunrise_start ({0}) is greater than sunrise_end ({1})",
config.sunrise_start, config.sunrise_end
)));
}
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, config: &Config) {
trace!("apply_settings({temp}, {gamma})");
let last_temp = LAST_TEMP.load(Ordering::SeqCst);
let last_gamma = LAST_GAMMA.load(Ordering::SeqCst);
if last_temp == temp && last_gamma == gamma {
trace!("Settings unchanged, skipping application");
return;
}
debug!("applying temperature: {temp}");
debug!("applying gamma: {gamma}");
if temp != last_temp {
match config.temp_backend {
TempBackend::Hyprctl => {
let _ = Command::new("hyprctl")
.args(["hyprsunset", "temperature", &temp.to_string()])
.output();
trace!("hyprctl hyprsunset temperature {temp}");
}
TempBackend::Redshift => {
let _ = Command::new("redshift")
.args(["-o", "-t", &temp.to_string()])
.output();
trace!("redshift -o -t {temp}");
}
TempBackend::Xsct => {
let _ = Command::new("xsct").args([&temp.to_string()]).output();
trace!("xsct {temp}");
}
TempBackend::None => {}
TempBackend::Gammastep => {
let _ = Command::new("gammastep")
.args(["-O", &temp.to_string()])
.output();
}
}
LAST_TEMP.store(temp, Ordering::SeqCst);
}
if gamma != last_gamma {
match config.gamma_backend {
GammaBackend::Hyprctl => {
let _ = Command::new("hyprctl")
.args(["hyprsunset", "gamma", &gamma.to_string()])
.output();
trace!("hyprctl hyprsunset gamma {gamma}");
}
GammaBackend::Ddcutil => {
let _ = Command::new("ddcutil")
.args(["setvcp", "10", &gamma.to_string()])
.output();
trace!("ddcutil setvcp 10 {gamma}");
}
GammaBackend::Gammastep => {
let _ = Command::new("gammastep")
.args(["-O", "6500", "-g", &(&gamma / 100).to_string()])
.output();
trace!("gammastep -O 6500 -g {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::Redshift => {
let _ = Command::new("redshift")
.args(["-O", "6500", "-g", &(&gamma / 100).to_string()])
.output();
trace!("redshift -O 6500 -g {gamma}");
}
GammaBackend::None => {}
}
LAST_GAMMA.store(gamma, Ordering::SeqCst);
}
}
pub fn reset_cache() {
LAST_TEMP.store(0, Ordering::SeqCst);
LAST_GAMMA.store(0, Ordering::SeqCst);
}