410 lines
14 KiB
Rust
410 lines
14 KiB
Rust
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::{
|
|
apply_settings, compute_settings, debug, error, info, trace, warn, Config, GammaBackend, TempBackend
|
|
};
|
|
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(¬ify)).await;
|
|
}
|
|
} else {
|
|
// File is new, reload to capture initial state
|
|
debug!("Config file detected, reloading...");
|
|
reload_config(Arc::clone(¬ify)).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() {
|
|
let args: Vec<String> = std::env::args().collect();
|
|
|
|
if args.len() > 1 && (args[1] == "--version" || args[1] == "-v") {
|
|
println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
|
|
return;
|
|
}
|
|
|
|
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()
|
|
};
|
|
|
|
// TODO: add a map to not check for binaries twice?
|
|
match cfg.gamma_backend {
|
|
GammaBackend::Hyprctl => check_binary("hyprctl"),
|
|
GammaBackend::Ddcutil => check_binary("ddcutil"),
|
|
GammaBackend::Xsct => check_binary("xsct"),
|
|
GammaBackend::Redshift => check_binary("redshift"),
|
|
GammaBackend::Gammastep => check_binary("gammastep"),
|
|
GammaBackend::None => {}
|
|
}
|
|
|
|
match cfg.temp_backend {
|
|
TempBackend::Hyprctl => check_binary("hyprctl"),
|
|
TempBackend::Gammastep => check_binary("gammastep"),
|
|
TempBackend::Xsct => check_binary("xsct"),
|
|
TempBackend::Redshift => check_binary("redshift"),
|
|
TempBackend::None => {}
|
|
}
|
|
|
|
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(¬ify);
|
|
smol::spawn(async move {
|
|
socket_server(disabled, notify).await;
|
|
})
|
|
.detach();
|
|
}
|
|
|
|
// Spawn config reloader
|
|
{
|
|
let notify = Arc::clone(¬ify);
|
|
smol::spawn(async move {
|
|
config_realoader(notify).await;
|
|
})
|
|
.detach();
|
|
}
|
|
|
|
// Spawn timer
|
|
{
|
|
let notify = Arc::clone(¬ify);
|
|
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);
|
|
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,
|
|
);
|
|
} else {
|
|
let now = get_time();
|
|
let (temp, gamma) = compute_settings(now, &*config_guard().await);
|
|
apply_settings(temp, gamma, &*config_guard().await);
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
fn check_binary(binary: &str) {
|
|
if !is_binary_available(binary) {
|
|
error!("{binary} is not available, exiting.");
|
|
std::process::exit(1);
|
|
}
|
|
}
|