386 lines
14 KiB
Rust
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);
|
|
}
|