#[cfg(not(any(
target_os = "linux",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
)))]
compile_error!("This crate is not supported on this platform");
use std::collections::BTreeSet;
use std::path::Path;
use std::process::{Command, Stdio};
use thiserror::Error;
use time::OffsetDateTime;
use crate::backend::gamma::GammaBackend;
use crate::backend::interpolation::{Interpolation, interpolate_value};
use crate::backend::temp::TempBackend;
pub mod backend;
pub mod log;
pub mod notify;
pub(crate) mod utils;
#[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_start_minutes: u8,
pub sunset_end: u8,
pub sunset_end_minutes: u8,
pub sunrise_start: u8,
pub sunrise_start_minutes: u8,
pub sunrise_end: u8,
pub sunrise_end_minutes: u8,
pub notification_timeout: u32,
pub disable_timeout: u32,
pub adjustment_frequency: u32, pub gamma_backend: [GammaBackend; 3],
pub temp_backend: [TempBackend; 3],
pub interpolation_temp: Interpolation,
pub interpolation_gamma: Interpolation,
}
impl Default for Config {
fn default() -> Self {
Self {
temp_day: 6500,
temp_night: 2500,
gamma_day: 100,
gamma_night: 95,
sunset_start: 19,
sunset_start_minutes: 0,
sunset_end: 22,
sunset_end_minutes: 0,
sunrise_start: 4,
sunrise_start_minutes: 0,
sunrise_end: 7,
sunrise_end_minutes: 0,
disable_timeout: 0,
adjustment_frequency: 300,
notification_timeout: 5000,
gamma_backend: [
GammaBackend::Hyprctl,
GammaBackend::None,
GammaBackend::None,
],
temp_backend: [TempBackend::Hyprctl, TempBackend::None, TempBackend::None],
interpolation_temp: Interpolation::Linear,
interpolation_gamma: Interpolation::Linear,
}
}
}
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("io error: {0}")]
IoError(std::io::Error),
#[error("parse int error: {0}")]
ParseIntError(std::num::ParseIntError),
#[error("invalid temperature: {0}")]
InvalidTemperature(u16),
#[error("invalid time: {0}:{1}")]
InvalidTime(u8, u8),
#[error("sunset_start, sunset_end, sunrise_start, and sunrise_end are all required")]
NoTime,
#[error("invalid gamma: {0}")]
InvalidGamma(u16),
#[error("invalid gamma backend: {0}")]
InvalidGammaBackend(String),
#[error("invalid temp backend: {0}")]
InvalidTempBackend(String),
#[error("invalid interpolation: {0}")]
InvalidInterpolation(String),
#[error("duplicate key: {0}")]
DuplicateKey(String),
#[error("unavailable command: {0}")]
UnavailableCommand(String),
}
impl From<std::io::Error> for ConfigError {
fn from(err: std::io::Error) -> Self {
Self::IoError(err)
}
}
impl From<std::num::ParseIntError> for ConfigError {
fn from(err: std::num::ParseIntError) -> Self {
Self::ParseIntError(err)
}
}
impl std::str::FromStr for Config {
type Err = ConfigError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
#[inline]
fn unquote(s: &str) -> &str {
s.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.unwrap_or_else(|| s.trim())
}
trace!("Config::from_str(...)");
let mut config = Self::default();
let mut seen_keys: BTreeSet<&str> = BTreeSet::new();
let mut seen_time_keys: BTreeSet<&str> = BTreeSet::new();
let mut current_section: &str = "";
for line in s
.lines()
.map(str::trim)
.filter(|l| !l.is_empty())
.filter(|l| !l.starts_with('#'))
{
if line.starts_with('[') && line.contains(']') {
if let Some(end) = line.find(']') {
current_section = line[1..end].trim();
}
seen_keys.clear();
} else if let Some((key, value)) = line.split_once('=') {
let key_trimmed = unquote(key);
if seen_keys.contains(key_trimmed) {
return Err(ConfigError::DuplicateKey(format!(
"Duplicate key '{key_trimmed}' in section '{current_section}'",
)));
}
seen_keys.insert(key_trimmed);
let comment_start = value.find('#');
let value = comment_start.map_or_else(
|| value.trim().replace('"', ""),
|cmnt_idx| value[..cmnt_idx].trim().replace('"', ""),
);
match current_section {
"" => match key_trimmed {
"notification_timeout" => {
config.notification_timeout = value.parse::<u32>()?;
},
"disable_timeout" => {
config.disable_timeout = value.parse::<u32>()?;
},
"adjustment_frequency" => {
config.adjustment_frequency = value.parse::<u32>()?;
},
"gamma_backend" => {
if !value.contains(',') {
let backend = GammaBackend::from_str(&value)
.map_err(ConfigError::InvalidGammaBackend)?;
config.gamma_backend =
[backend, GammaBackend::None, GammaBackend::None];
} else {
warn!("EXPERIMENTAL: multiple backend mode");
let mut backends = [GammaBackend::None; 3];
for (i, part) in value.split(',').enumerate() {
if i >= backends.len() {
return Err(ConfigError::InvalidGammaBackend(value));
}
backends[i] = GammaBackend::from_str(part)
.map_err(ConfigError::InvalidGammaBackend)?;
}
config.gamma_backend = backends;
}
},
"temp_backend" => {
if !value.contains(',') {
let backend = TempBackend::from_str(&value)
.map_err(ConfigError::InvalidTempBackend)?;
config.temp_backend =
[backend, TempBackend::None, TempBackend::None];
} else {
warn!("EXPERIMENTAL: multiple backend mode");
let mut backends = [TempBackend::None; 3];
for (i, part) in value.split(',').enumerate() {
if i >= backends.len() {
return Err(ConfigError::InvalidTempBackend(value));
}
backends[i] = TempBackend::from_str(part)
.map_err(ConfigError::InvalidTempBackend)?;
}
config.temp_backend = backends;
}
},
k => {
warn!("unknown key in config: {k}");
},
},
"gamma" => match key_trimmed {
"day" => {
let parsed = value.parse::<u16>()?;
if 0 < parsed && parsed <= 100 {
config.gamma_day = parsed;
} else {
return Err(ConfigError::InvalidGamma(parsed));
}
},
"night" => {
let parsed = value.parse::<u16>()?;
if 0 < parsed && parsed <= 100 {
config.gamma_night = parsed;
} else {
return Err(ConfigError::InvalidGamma(parsed));
}
},
k => {
warn!("unknown key in gamma section: {k}");
},
},
"temp" => match key_trimmed {
"day" => {
let parsed = value.parse::<u16>()?;
if (1000..=20000).contains(&parsed) {
config.temp_day = parsed;
} else {
return Err(ConfigError::InvalidTemperature(parsed));
}
},
"night" => {
let parsed = value.parse::<u16>()?;
if (1000..=20000).contains(&parsed) {
config.temp_night = parsed;
} else {
return Err(ConfigError::InvalidTemperature(parsed));
}
},
k => {
warn!("unknown key in temp section: {k}");
},
},
"time" => {
fn parse_time(value: &str, key: &str) -> Result<(u8, u8), ConfigError> {
if let Some(colon) = value.find(':') {
let hour = value[..colon].parse::<u8>()?;
let minute = value[colon + 1..].parse::<u8>()?;
if (0..=23).contains(&hour) && (0..=59).contains(&minute) {
Ok((hour, minute))
} else {
Err(ConfigError::InvalidTime(hour, minute))
}
} else {
warn!("missing minutes in {}, assuming 00", key);
let hour = value.parse::<u8>()?;
if (0..=23).contains(&hour) {
Ok((hour, 0))
} else {
Err(ConfigError::InvalidTime(hour, 0))
}
}
}
seen_time_keys.insert(key_trimmed);
match key_trimmed {
"sunset_start" => {
(config.sunset_start, config.sunset_start_minutes) =
parse_time(&value, "sunset_start")?;
},
"sunset_end" => {
(config.sunset_end, config.sunset_end_minutes) =
parse_time(&value, "sunset_end")?;
},
"sunrise_start" => {
(config.sunrise_start, config.sunrise_start_minutes) =
parse_time(&value, "sunrise_start")?;
},
"sunrise_end" => {
(config.sunrise_end, config.sunrise_end_minutes) =
parse_time(&value, "sunrise_end")?;
},
"interpolation_temp" => {
config.interpolation_temp = value
.parse::<Interpolation>()
.map_err(ConfigError::InvalidInterpolation)?;
},
"interpolation_gamma" => {
config.interpolation_gamma = value
.parse::<Interpolation>()
.map_err(ConfigError::InvalidInterpolation)?;
},
k => {
warn!("unknown key in time section: {k}");
},
}
},
s => {
warn!("unknown section: {s}");
},
}
}
}
if !seen_time_keys.contains("sunset_start")
|| !seen_time_keys.contains("sunset_end")
|| !seen_time_keys.contains("sunrise_start")
|| !seen_time_keys.contains("sunrise_end")
{
return Err(ConfigError::NoTime);
}
Ok(config)
}
}
impl Config {
pub fn load<P: AsRef<Path> + std::fmt::Debug>(path: P) -> Result<Self, ConfigError> {
trace!("Config::load({path:?})");
let file = std::fs::read_to_string(path)?;
file.parse::<Self>()
}
#[must_use]
pub fn default_as_str() -> &'static str {
r#"# how much time does it take for notifications to disappear
notification_timeout = 5000
# how much time does hinoirisetr wait between adjustments in seconds
adjustment_frequency = 300
# how long does it take for hinoirisetr to enable itself after disabling
disable_timeout = 0
# supported backends:
# acpi
# hyprctl
# wayland
# ddcutil
# xsct
# none
# can be a comma-separated list of up to 3 backends e.g. "ddcutil,xsct,acpi"
gamma_backend = "wayland"
# supported backends:
# hyprctl
# wayland
# xsct
# none
# can be a comma-separated list of up to 3 backends e.g. "hyprctl,xsct,none"
temp_backend = "wayland"
# gamma during the night and the day, specified as a percentage
[gamma]
day = 100
night = 95
# temperature during the night and the day, specified in Kelvin
[temp]
day = 6500
night = 2500
# time in HH:MM format
[time]
sunset_start = 19:00
sunset_end = 22:00
sunrise_start = 04:00
sunrise_end = 07:00
# interpolation mode for temperature and gamma factor (during sunset or sunrise)
# supported modes:
# linear
# cubic
# cosine
# exponential
interpolation_temp = "linear"
interpolation_gamma = "linear""#
}
}
#[must_use]
pub fn compute_settings(now: OffsetDateTime, config: &Config) -> (u16, u16) {
trace!("compute_settings({now:?})");
let tod = now.time();
let time_in_hours = f64::from(tod.hour()) + f64::from(tod.minute()) / 60.0;
trace!("time_in_hours: {time_in_hours}");
let sunset_start =
f64::from(config.sunset_start) + f64::from(config.sunset_start_minutes) / 60.0;
let mut sunset_end = f64::from(config.sunset_end) + f64::from(config.sunset_end_minutes) / 60.0;
if sunset_end < f64::from(config.sunset_start) + f64::from(config.sunset_start_minutes) / 60.0 {
sunset_end += 24.0;
}
let sunrise_start =
f64::from(config.sunrise_start) + f64::from(config.sunrise_start_minutes) / 60.0;
let mut sunrise_end =
f64::from(config.sunrise_end) + f64::from(config.sunrise_end_minutes) / 60.0;
if sunrise_end
< f64::from(config.sunrise_start) + f64::from(config.sunrise_start_minutes) / 60.0
{
sunrise_end += 24.0;
}
let time_for_sunset = if time_in_hours
< f64::from(config.sunset_start) + f64::from(config.sunset_start_minutes) / 60.0
{
time_in_hours + 24.0
} else {
time_in_hours
};
let time_for_sunrise = if time_in_hours
< f64::from(config.sunrise_start) + f64::from(config.sunrise_start_minutes) / 60.0
{
time_in_hours + 24.0
} else {
time_in_hours
};
trace!("sunset_start: {sunset_start}, sunset_end: {sunset_end}");
trace!("sunrise_start: {sunrise_start}, sunrise_end: {sunrise_end}");
if (time_for_sunset >= sunset_start) && (time_for_sunset <= sunset_end) {
trace!("time_in_hours is within sunset");
let factor =
((time_for_sunset - sunset_start) / (sunset_end - sunset_start)).clamp(0.0, 1.0);
(
interpolate_value(
config.temp_day,
config.temp_night,
factor,
config.interpolation_temp,
),
interpolate_value(
config.gamma_day,
config.gamma_night,
factor,
config.interpolation_gamma,
),
)
} else if (time_for_sunrise >= sunrise_start) && (time_for_sunrise <= sunrise_end) {
trace!("time_in_hours is within sunrise");
let factor = 1.0
- ((time_for_sunrise - sunrise_start) / (sunrise_end - sunrise_start)).clamp(0.0, 1.0);
(
interpolate_value(
config.temp_day,
config.temp_night,
factor,
config.interpolation_temp,
),
interpolate_value(
config.gamma_day,
config.gamma_night,
factor,
config.interpolation_gamma,
),
)
} else if time_in_hours > sunset_end || time_in_hours < sunrise_start {
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)
}
}
pub fn reset_cache() {
backend::temp::reset_cache();
backend::gamma::reset_cache();
}
fn ensure_hyprsunset_running() -> Result<(), String> {
trace!("Ensuring hyprsunset is running...");
if !is_process_running("hyprsunset") {
info!("hyprsunset is not running. Attempting to start it...");
Command::new("hyprsunset")
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map_err(|e| format!("Failed to start hyprsunset: {e}"))?;
std::thread::sleep(std::time::Duration::from_secs(1));
if is_process_running("hyprsunset") {
return Ok(());
}
return Err("Failed to start hyprsunset".to_string());
}
Ok(())
}
fn is_process_running(process_name: &str) -> bool {
Command::new("pgrep")
.arg("-x")
.arg(process_name)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|status| status.success())
.unwrap_or(false)
}