hinoirisetr 0.3.0

A daemon to dim the screen at night
Documentation
use std::env;
use std::path::PathBuf;
use std::sync::atomic::Ordering::SeqCst;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, OnceLock};
use std::time::{Duration, UNIX_EPOCH};

use hinoirisetr::notify::InitializedNotificationSystem;
use hinoirisetr::time::Time;
use hinoirisetr::{Config, apply_settings, compute_settings, debug, error, info, trace, warn};
use smol::Timer;
use smol::channel::{Sender, unbounded};
use smol::io::{AsyncBufReadExt, BufReader};
use smol::lock::{RwLock, RwLockReadGuard};
use smol::net::unix::UnixListener;
use smol::stream::StreamExt;

const SOCKET_PATH: &str = "/tmp/hinoirisetr.sock";

static CONFIG: OnceLock<Arc<RwLock<Config>>> = OnceLock::new();
static LAST_MODIFIED: AtomicU64 = AtomicU64::new(0);

enum NotifyState {
    Enabled(InitializedNotificationSystem),
    Disabled,
}

async fn config_realoader(notify: Arc<Sender<()>>) {
    debug!("config reloader started");
    // Config polling every 5 seconds
    loop {
        trace!("config poll tick");
        let config_path = get_config_path();
        if config_path.exists() {
            if let Ok(current_modified) = std::fs::metadata(&config_path)
                .and_then(|m| m.modified())
                .map(|t| t.duration_since(UNIX_EPOCH).unwrap().as_secs())
            {
                let last: u64 = LAST_MODIFIED.load(Ordering::SeqCst);
                if 0 != last {
                    if current_modified > last {
                        trace!("{current_modified}");
                        trace!("{last}");
                        debug!("Config file modified, reloading...");
                        reload_config(Arc::clone(&notify)).await;
                    }
                } else {
                    // File is new, reload to capture initial state
                    debug!("Config file detected, reloading...");
                    reload_config(Arc::clone(&notify)).await;
                }
            }
        }
        Timer::after(Duration::from_secs(5)).await;
    }
}

async fn socket_server(disabled: Arc<AtomicBool>, notify: Arc<Sender<()>>) {
    let listener = UnixListener::bind(SOCKET_PATH).expect("Failed to bind socket");
    trace!("socket server bound");
    let notification: NotifyState =
        match hinoirisetr::notify::InitializedNotificationSystem::new("hinoirisetr") {
            Ok(not) => NotifyState::Enabled(not),
            Err(_) => {
                info!("libnotify not found, disabling 'status_notify' command");
                NotifyState::Disabled
            }
        };

    loop {
        let (stream, _) = listener.accept().await.unwrap();
        let reader = BufReader::new(stream);
        let mut lines = reader.lines();

        trace!("socket server accepted connection");

        if let Some(Ok(line)) = lines.next().await {
            match line.trim() {
                "disable" => {
                    trace!("disable dispatched");
                    disabled.store(true, Ordering::SeqCst);
                    let _ = notify.send(()).await;
                    debug!("dimming is disabled");
                }
                "enable" => {
                    trace!("enable dispatched");
                    disabled.store(false, Ordering::SeqCst);
                    let _ = notify.send(()).await;
                    debug!("dimming is enabled");
                }
                "toggle" => {
                    trace!("toggle dispatched");
                    let now = !disabled.load(Ordering::SeqCst);
                    disabled.store(now, Ordering::SeqCst);
                    let _ = notify.send(()).await;
                    debug!("dimming is {}", if now { "enabled" } else { "disabled" });
                }
                "status" => {
                    trace!("status dispatched");
                    // compute current temp/gamma
                    let now = get_time();
                    let (cur_temp, cur_gamma) = compute_settings(now, &*config_guard().await);

                    info!(
                        "dimming is {} — temp: {}K, gamma: {}%",
                        if disabled.load(Ordering::SeqCst) {
                            "disabled"
                        } else {
                            "enabled"
                        },
                        cur_temp,
                        cur_gamma
                    );
                }
                "reload" => {
                    trace!("reload dispatched");
                    reload_config(notify.clone()).await;
                }
                "status_notify" => {
                    trace!("status_notify dispatched");
                    let now = get_time();
                    let (cur_temp, cur_gamma) = compute_settings(now, &*config_guard().await);

                    let body = if disabled.load(Ordering::SeqCst) {
                        "disabled".to_string()
                    } else {
                        format!("temp: {cur_temp}K, gamma: {cur_gamma}%")
                    };

                    match notification {
                        NotifyState::Enabled(ref not) => {
                            trace!("notify notification enabled");
                            let timeout = config_guard().await.notification_timeout;
                            match not.show_notification(
                                "Sunsetting",
                                &body,
                                "notification-icon",
                                timeout as i32,
                            ) {
                                Ok(_) => {}
                                Err(e) => error!("Failed to show notification: {e:?}"),
                            };
                        }
                        NotifyState::Disabled => {
                            trace!("notify notification disabled");
                        }
                    }
                }
                _ => error!("unknown command: {}", line.trim()),
            }
        }
    }
}

fn main() {
    smol::block_on(async {
        match env::var("RUST_LOG") {
            Ok(val) => match val.parse::<hinoirisetr::log::LogLevel>() {
                Ok(level) => hinoirisetr::log::set_log_level(level),
                Err(err) => error!("Failed to parse RUST_LOG: {err}"),
            },
            Err(_) => {
                if cfg!(debug_assertions) {
                    hinoirisetr::log::set_log_level(hinoirisetr::log::LogLevel::Debug);
                } else {
                    hinoirisetr::log::set_log_level(hinoirisetr::log::LogLevel::Info);
                }
            }
        }

        info!("starting the daemon");

        if !is_binary_available("hyprctl") {
            error!("hyprctl is not available, exiting.");
            std::process::exit(1);
        }

        let disabled = Arc::new(AtomicBool::new(false));
        let (notify_s, notify_r) = unbounded::<()>();
        let notify = Arc::new(notify_s);

        // load config
        let config_path = get_config_path();
        let cfg: Config = if config_path.exists() {
            debug!("Config file found, loading...");
            LAST_MODIFIED.store(
                std::fs::metadata(&config_path)
                    .and_then(|m| m.modified())
                    .map(|t| t.duration_since(UNIX_EPOCH).unwrap().as_secs())
                    .unwrap_or(0),
                SeqCst,
            );
            match Config::load(&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(_) => {
                    error!("Another instance is running.");
                    std::process::exit(1);
                }
                Err(_) => {
                    warn!("Stale socket found, removing.");
                    let _ = std::fs::remove_file(SOCKET_PATH);
                }
            }
        }

        // Spawn control socket server
        {
            let disabled = Arc::clone(&disabled);
            let notify = Arc::clone(&notify);
            smol::spawn(async move {
                socket_server(disabled, notify).await;
            })
            .detach();
        }

        // Spawn config reloader
        {
            let notify = Arc::clone(&notify);
            smol::spawn(async move {
                config_realoader(notify).await;
            })
            .detach();
        }

        // Spawn timer
        {
            let notify = Arc::clone(&notify);
            smol::spawn(async move {
                loop {
                    Timer::after(Duration::from_secs(300)).await;
                    let _ = notify.send(()).await;
                }
            })
            .detach();
        }

        // Signal handling
        // let mut sigint = signal(SignalKind::interrupt()).unwrap();
        // let mut sigterm = signal(SignalKind::terminate()).unwrap();

        // set initial settings
        {
            let now = get_time();
            let (temp, gamma) = compute_settings(now, &*config_guard().await);
            apply_settings(temp, gamma, config_guard().await.gamma_backend);
            trace!("initial settings applied: {temp}K, {gamma}%");
        }

        // Main loop with shutdown support
        loop {
            if disabled.load(Ordering::SeqCst) {
                apply_settings(
                    config_guard().await.temp_day,
                    config_guard().await.gamma_day,
                    config_guard().await.gamma_backend,
                );
            } else {
                let now = get_time();
                let (temp, gamma) = compute_settings(now, &*config_guard().await);
                apply_settings(temp, gamma, config_guard().await.gamma_backend);
            }

            let _ = notify_r.recv().await;
        }
        //     _ = sigint.recv() => {
        //         info!("Received SIGINT, shutting down...");
        //     },
        //     _ = sigterm.recv() => {
        //         info!("Received SIGTERM, shutting down...");
        //     },
        // }

        // Cleanup the socket file on shutdown
        // if std::path::Path::new(SOCKET_PATH).exists() {
        //     match std::fs::remove_file(SOCKET_PATH) {
        //         Ok(_) => info!("Socket file {SOCKET_PATH} removed."),
        //         Err(e) => warn!("Failed to remove socket file {SOCKET_PATH}: {e}"),
        //     }
        // }
    })
}

// Function to handle config reloading
async fn reload_config(notify: Arc<Sender<()>>) {
    trace!("reload_config called");
    let config_handle = config_handle();
    let mut config = config_handle.write().await;
    let config_path = get_config_path();
    match Config::load(&config_path) {
        Ok(cfg) => {
            debug!("Config file reloaded successfully");
            *config = cfg;
            trace!("new config: {:#?}", config);

            let new_modified = std::fs::metadata(&config_path)
                .and_then(|m| m.modified())
                .map(|t| t.duration_since(UNIX_EPOCH).unwrap().as_secs())
                .unwrap_or(0);
            trace!("new_modified: {new_modified:?}");
            LAST_MODIFIED.store(new_modified, SeqCst);
            let _ = notify.send(()).await;
        }
        Err(err) => {
            error!("Failed to reload config: {err:?}");
            warn!("Retaining current config");
        }
    }
}
fn is_binary_available(binary_name: &str) -> bool {
    use std::fs;
    if let Ok(paths) = env::var("PATH") {
        for path in env::split_paths(&paths) {
            let full_path = path.join(binary_name);
            if full_path.exists()
                && fs::metadata(&full_path)
                    .map(|m| m.is_file())
                    .unwrap_or(false)
            {
                return true;
            }
        }
    }
    false
}

#[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() -> RwLockReadGuard<'static, Config> {
    CONFIG.get().expect("config not init").read().await
}

fn config_handle() -> Arc<RwLock<Config>> {
    CONFIG.get().expect("config not init").clone()
}

// async fn get_config() -> Config {
//     let lock = CONFIG.get().expect("config not initialized").read().await;
//     lock.clone()
// }

fn get_time() -> Time {
    Time::now().expect("Failed to get local time")
}