diff --git a/Cargo.lock b/Cargo.lock index f160cf6..32d6657 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -544,7 +544,9 @@ dependencies = [ "env_logger", "log", "notify-rust", + "serde", "tokio", + "toml", ] [[package]] @@ -997,6 +999,15 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1140,11 +1151,26 @@ dependencies = [ "syn", ] +[[package]] +name = "toml" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + [[package]] name = "toml_datetime" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" @@ -1153,6 +1179,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ "indexmap", + "serde", + "serde_spanned", "toml_datetime", "winnow", ] diff --git a/Cargo.toml b/Cargo.toml index 155e74d..613a78d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,8 @@ chrono = "0.4.40" env_logger = "0.11.8" log = "0.4.27" notify-rust = "4.11.7" +serde = { version = "1.0", features = ["derive"] } +toml = "0.8" tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time","io-util","net","signal" ] } [profile.release] diff --git a/flake.nix b/flake.nix index a93346a..4fa6dc7 100644 --- a/flake.nix +++ b/flake.nix @@ -21,7 +21,12 @@ pkgs = import nixpkgs { inherit system overlays; }; - rust = pkgs.rust-bin.nightly.latest.default; + rust = pkgs.rust-bin.selectLatestNightlyWith ( + toolchain: + toolchain.default.override { + extensions = [ "rust-analyzer" ]; + } + ); in { devShells.default = pkgs.mkShell rec { diff --git a/src/lib.rs b/src/lib.rs index b67f935..9e86245 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,64 +1,109 @@ //! hinoirisetr library //! Contains core logic for computing temperature and gamma and applying settings. +use std::fs; +use std::path::Path; use std::process::Command; use chrono::{DateTime, Local, Timelike}; - use log::{debug, trace}; +use serde::Deserialize; -// Constants for colors and timings -pub const TEMP_DAY: i32 = 6500; -pub const TEMP_NIGHT: i32 = 2700; +#[derive(Debug, Deserialize, Copy, Clone)] +#[serde(default)] +pub struct Config { + pub temp_day: u16, + pub temp_night: u16, -pub const GAMMA_DAY: i32 = 100; -pub const GAMMA_NIGHT: i32 = 95; + pub gamma_day: u16, + pub gamma_night: u16, -pub const SUNSET_START: u32 = 19; -pub const SUNSET_END: u32 = 22; -pub const SUNRISE_START: u32 = 14; -pub const SUNRISE_END: u32 = 17; + pub sunset_start: u8, + pub sunset_end: u8, + pub sunrise_start: u8, + pub sunrise_end: u8, + + pub notification_timeout: u32, +} + +impl Default for Config { + fn default() -> Self { + Self { + temp_day: 6500, + temp_night: 2700, + gamma_day: 100, + gamma_night: 95, + sunset_start: 19, + sunset_end: 22, + sunrise_start: 4, + sunrise_end: 7, + notification_timeout: 5000, + } + } +} + +impl Config { + pub fn load + std::fmt::Debug>( + path: P, + ) -> Result> { + trace!("Config::load({path:?})"); + let content = fs::read_to_string(path)?; + let config = toml::from_str(&content)?; + Ok(config) + } +} /// Linearly interpolate between start and end by factor [0.0, 1.0] -pub fn interpolate(start: i32, end: i32, factor: f64) -> i32 { +pub fn interpolate(start: u16, end: u16, factor: f64) -> u16 { trace!("interpolate({start}, {end}, {factor})"); - (start as f64 + (end - start) as f64 * factor).round() as i32 + 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: DateTime) -> (i32, i32) { +pub fn compute_settings(now: DateTime, 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 >= SUNSET_START as f64) && (time_in_hours <= SUNSET_END as f64) { + 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 - SUNSET_START as f64) / (SUNSET_END - SUNSET_START) as f64) + let factor = ((time_in_hours - config.sunset_start as f64) + / (config.sunset_end - config.sunset_start) as f64) .clamp(0.0, 1.0); ( - interpolate(TEMP_DAY, TEMP_NIGHT, factor), - interpolate(GAMMA_DAY, GAMMA_NIGHT, factor), + interpolate(config.temp_day, config.temp_night, factor), + interpolate(config.gamma_day, config.gamma_night, factor), ) - } else if (time_in_hours >= SUNRISE_START as f64) && (time_in_hours <= SUNRISE_END as f64) { + } 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 - SUNRISE_START as f64) / (SUNRISE_END - SUNRISE_START) as f64) + - ((time_in_hours - config.sunrise_start as f64) + / (config.sunrise_end - config.sunrise_start) as f64) .clamp(0.0, 1.0); ( - interpolate(TEMP_DAY, TEMP_NIGHT, factor), - interpolate(GAMMA_DAY, GAMMA_NIGHT, factor), + interpolate(config.temp_day, config.temp_night, factor), + interpolate(config.gamma_day, config.gamma_night, factor), ) - } else if time_in_hours > SUNSET_END as f64 || time_in_hours < SUNRISE_START as f64 { + } 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"); - (TEMP_NIGHT, GAMMA_NIGHT) + (config.temp_night, config.gamma_night) } else { trace!("time_in_hours is within day"); - (TEMP_DAY, GAMMA_DAY) + (config.temp_day, config.gamma_day) } } /// Apply given temperature (Kelvin) and gamma (%) via hyprctl commands -pub fn apply_settings(temp: i32, gamma: i32) { +pub fn apply_settings(temp: u16, gamma: u16) { trace!("apply_settings({temp}, {gamma})"); debug!("applying temperature: {temp}"); debug!("applying gamma: {gamma}"); diff --git a/src/main.rs b/src/main.rs index 40ad8f4..771fce1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,22 @@ -use std::sync::Arc; +use std::env; +use std::path::PathBuf; use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, OnceLock}; use std::time::Duration; use chrono::Local; -use hinoirisetr::{apply_settings, compute_settings}; +use hinoirisetr::{Config, apply_settings, compute_settings}; use log::{debug, error, info, trace, warn}; use notify_rust::Notification; use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::net::UnixListener; use tokio::signal::unix::{SignalKind, signal}; -use tokio::sync::Notify; +use tokio::sync::{Notify, RwLock}; use tokio::time::sleep; const SOCKET_PATH: &str = "/tmp/hinoirisetr.sock"; -const NOTIFICATION_TIMEOUT: u32 = 5000; + +static CONFIG: OnceLock>> = OnceLock::new(); async fn socket_server(disabled: Arc, notify: Arc) { let listener = UnixListener::bind(SOCKET_PATH).expect("Failed to bind socket"); @@ -50,7 +53,7 @@ async fn socket_server(disabled: Arc, notify: Arc) { trace!("status dispatched"); // compute current temp/gamma let now = Local::now(); - let (cur_temp, cur_gamma) = compute_settings(now); + let (cur_temp, cur_gamma) = compute_settings(now, &*config_guard().await); info!( "dimming is {} — temp: {}K, gamma: {}%", @@ -66,7 +69,7 @@ async fn socket_server(disabled: Arc, notify: Arc) { "status_notify" => { trace!("status_notify dispatched"); let now = Local::now(); - let (cur_temp, cur_gamma) = compute_settings(now); + let (cur_temp, cur_gamma) = compute_settings(now, &*config_guard().await); let body = if disabled.load(Ordering::SeqCst) { "disabled".to_string() @@ -77,7 +80,9 @@ async fn socket_server(disabled: Arc, notify: Arc) { match Notification::new() .summary("Sunsetting") .body(body.as_str()) - .timeout(notify_rust::Timeout::Milliseconds(NOTIFICATION_TIMEOUT)) + .timeout(notify_rust::Timeout::Milliseconds( + config_guard().await.notification_timeout, + )) .show_async() .await { @@ -101,6 +106,24 @@ async fn main() { let disabled = Arc::new(AtomicBool::new(false)); let notify = Arc::new(Notify::new()); + let cfg: Config = if get_config_path().exists() { + debug!("Config file found, loading..."); + match Config::load(get_config_path()) { + Ok(cfg) => cfg, + Err(err) => { + error!("Failed to load config: {err}"); + warn!("Using default config."); + Config::default() + } + } + } else { + warn!("Config file not found, using default config."); + warn!("Config path {}", get_config_path().display()); + Config::default() + }; + + CONFIG.set(Arc::new(RwLock::new(cfg))).unwrap(); + if std::path::Path::new(SOCKET_PATH).exists() { match std::os::unix::net::UnixStream::connect(SOCKET_PATH) { Ok(_) => { @@ -132,10 +155,10 @@ async fn main() { _ = async { loop { if disabled.load(Ordering::SeqCst) { - apply_settings(hinoirisetr::TEMP_DAY, hinoirisetr::GAMMA_DAY); + apply_settings(config_guard().await.temp_day, config_guard().await.gamma_day); } else { let now = Local::now(); - let (temp, gamma) = compute_settings(now); + let (temp, gamma) = compute_settings(now,&*config_guard().await); apply_settings(temp, gamma); } @@ -161,3 +184,35 @@ async fn main() { } } } + +#[inline] +fn get_config_path() -> PathBuf { + if cfg!(target_os = "windows") { + let username = env::var("USERNAME").unwrap_or_else(|_| "Default".to_string()); + PathBuf::from(format!( + "C:\\Users\\{username}\\AppData\\Local\\hinoirisetr.toml" + )) + } else { + let xdg_config_home = env::var("XDG_CONFIG_HOME").ok(); + let home = env::var("HOME").ok(); + let user = env::var("USER").unwrap_or_else(|_| "default".to_string()); + + xdg_config_home + .map(|x| PathBuf::from(format!("{x}/hinoirisetr.toml"))) + .or_else(|| home.map(|h| PathBuf::from(format!("{h}/.config/hinoirisetr.toml")))) + .unwrap_or_else(|| PathBuf::from(format!("/home/{user}/.config/hinoirisetr.toml"))) + } +} + +async fn config_guard() -> tokio::sync::RwLockReadGuard<'static, Config> { + CONFIG.get().expect("config not init").read().await +} + +// fn config_handle() -> Arc> { +// CONFIG.get().expect("config not init").clone() +// } + +// async fn get_config() -> Config { +// let lock = CONFIG.get().expect("config not initialized").read().await; +// lock.clone() +// }