hinoirisetr 0.1.0

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

use hinoirisetr::notify::InitializedNotificationSystem;
use hinoirisetr::time::Time;
use hinoirisetr::{Config, apply_settings, compute_settings, debug, error, info, trace, warn};
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::net::UnixListener;
use tokio::signal::unix::{SignalKind, signal};
use tokio::sync::{Notify, RwLock};
use tokio::time::sleep;

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

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

enum NotifyState {
    Enabled(InitializedNotificationSystem),
    Disabled,
}

async fn socket_server(disabled: Arc<AtomicBool>, notify: Arc<Notify>) {
    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 mut lines = BufReader::new(stream).lines();

        trace!("socket server accepted connection");

        if let Ok(Some(line)) = lines.next_line().await {
            match line.trim() {
                "disable" => {
                    trace!("disable dispatched");
                    disabled.store(true, Ordering::SeqCst);
                    notify.notify_one();
                    debug!("dimming is disabled");
                }
                "enable" => {
                    trace!("enable dispatched");
                    disabled.store(false, Ordering::SeqCst);
                    notify.notify_one();
                    debug!("dimming is enabled");
                }
                "toggle" => {
                    trace!("toggle dispatched");
                    let now = !disabled.load(Ordering::SeqCst);
                    disabled.store(now, Ordering::SeqCst);
                    notify.notify_one();
                    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");
                    let config_handle = config_handle();
                    let mut config = config_handle.write().await;
                    match Config::load(get_config_path()) {
                        Ok(cfg) => {
                            debug!("Config file found, loading...");
                            *config = cfg;
                            notify.notify_one();
                        }
                        Err(err) => {
                            error!("Failed to load config: {err}");
                            warn!("Using default config.");
                        }
                    }
                }
                "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_handle().read().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()),
            }
        }
    }
}

#[tokio::main]
async fn main() {
    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 = 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(_) => {
                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);
        tokio::spawn(async move {
            socket_server(disabled, notify).await;
        });
    }

    // 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);
        trace!("initial settings applied: {temp}K, {gamma}%");
    }

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

                tokio::select! {
                    _ = sleep(Duration::from_secs(300)) => {trace!("main loop tick");},
                    _ = notify.notified() => {trace!("main loop woke up via notify");},
                }
            }
        } => {},
        _ = 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}"),
        }
    }
}

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() -> tokio::sync::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")
}