use std::env;
use std::os::unix::fs::PermissionsExt;
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::{
Config, ConfigError, GammaBackend, TempBackend, apply_gamma, apply_temp, compute_settings,
debug, error, info, reset_cache, trace, warn,
};
use time::OffsetDateTime;
use tokio::io::{AsyncBufReadExt, AsyncWrite, AsyncWriteExt, BufReader};
use tokio::net::UnixListener;
use tokio::signal::unix::{SignalKind, signal};
use tokio::sync::{Notify, RwLock};
use tokio::time::sleep;
fn get_socket_path() -> PathBuf {
let username = env::var("USER").expect("USER environment variable not set");
let mut socket_path = PathBuf::from("/tmp");
socket_path.push(format!("hinoirisetr-{username}.sock"));
socket_path
}
static CONFIG: OnceLock<Arc<RwLock<Config>>> = OnceLock::new();
static LAST_MODIFIED: AtomicU64 = AtomicU64::new(0);
#[derive(Clone)]
enum NotifyState {
Enabled(InitializedNotificationSystem),
Disabled,
}
async fn write_response<W: AsyncWrite + Unpin>(
writer: &mut W,
response: &str,
) -> Result<(), Box<dyn std::error::Error>> {
writer.write_all(response.as_bytes()).await?;
writer.shutdown().await?;
Ok(())
}
fn get_status_string(
now: OffsetDateTime,
config: &Config,
disabled_temp: &AtomicBool,
disabled_gamma: &AtomicBool,
) -> String {
let (cur_temp, cur_gamma) = compute_settings(now, config);
format!(
"dimming - {}{}{}",
if config.temp_backend.iter().all(|b| *b == TempBackend::None) {
String::new()
} else {
format!(
"temp: {}",
if disabled_temp.load(Ordering::SeqCst) {
"disabled".to_string()
} else {
format!("{cur_temp}K")
}
)
},
if config.temp_backend.iter().any(|b| *b != TempBackend::None)
&& config
.gamma_backend
.iter()
.any(|b| *b != GammaBackend::None)
{
", "
} else {
""
},
if config
.gamma_backend
.iter()
.all(|b| *b == GammaBackend::None)
{
String::new()
} else {
format!(
"gamma: {}",
if disabled_gamma.load(Ordering::SeqCst) {
"disabled".to_string()
} else {
format!("{cur_gamma}%")
}
)
}
)
}
async fn config_reloader(notify: Arc<Notify>) {
debug!("config reloader started");
loop {
trace!("config poll tick");
let config_path = get_config_path();
if config_path.exists()
&& 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...");
let _ = reload_config(Arc::clone(¬ify)).await;
}
} else {
debug!("Config file detected, reloading...");
let _ = reload_config(Arc::clone(¬ify)).await;
}
}
sleep(Duration::from_secs(5)).await;
}
}
#[allow(clippy::too_many_arguments)]
async fn handle_flag_update<F, G, W>(
disabled_temp: &AtomicBool,
disabled_gamma: &AtomicBool,
set_temp: F,
set_gamma: G,
status: &str,
#[allow(unused_variables)] trace_msg: &str,
notify: &Notify,
writer: &mut W,
) where
F: FnOnce(&AtomicBool),
G: FnOnce(&AtomicBool),
W: AsyncWrite + Unpin,
{
trace!("{}", trace_msg);
set_temp(disabled_temp);
set_gamma(disabled_gamma);
notify.notify_one();
info!("{}", status);
if let Err(e) = write_response(writer, status).await {
error!("Failed to respond over socket: {e}");
}
}
macro_rules! toggle_flag {
($flag:expr, $notify:expr, $writer:expr, $name:expr, $trace_msg:expr) => {{
use std::sync::atomic::Ordering::SeqCst;
trace!("{}", $trace_msg);
let old = $flag.load(SeqCst);
let new_val = !old;
$flag.store(new_val, SeqCst);
$notify.notify_one();
let status = format!(
"{} dimming is {}",
$name,
if new_val { "disabled" } else { "enabled" }
);
info!("{}", status);
if let Err(e) = write_response(&mut $writer, &status).await {
error!("Failed to respond over socket: {e}");
}
}};
}
async fn socket_server(
disabled_temp: Arc<AtomicBool>,
disabled_gamma: Arc<AtomicBool>,
notify: Arc<Notify>,
) -> ! {
let listener = UnixListener::bind(get_socket_path()).expect("Failed to bind socket");
match std::fs::set_permissions(get_socket_path(), std::fs::Permissions::from_mode(0o600)) {
Ok(()) => {
trace!("socket file permissions set");
}
Err(e) => {
error!("Failed to set socket file permissions: {e}");
}
}
trace!("socket server bound");
let notification: NotifyState =
hinoirisetr::notify::InitializedNotificationSystem::new("hinoirisetr").map_or_else(
|_| {
info!("libnotify not found, disabling 'status_notify' command");
NotifyState::Disabled
},
NotifyState::Enabled,
);
let disabler_notify = Arc::new(Notify::new());
loop {
let (mut stream, _) = listener.accept().await.unwrap();
let (reader, mut writer) = stream.split();
let mut lines = BufReader::new(reader).lines();
trace!("socket server accepted connection");
if let Ok(Some(line)) = lines.next_line().await {
let config = get_config().await;
if config.disable_timeout > 0 {
match line.trim() {
"toggle" | "toggle_gamma" | "toggle_temp" | "disable" | "disable_gamma"
| "disable_temp" => {
let disabled_temp = disabled_temp.clone();
let disabled_gamma = disabled_gamma.clone();
let notification = notification.clone();
let notify = notify.clone();
let disabler_notify = disabler_notify.clone();
tokio::spawn(async move {
if !disabled_gamma.load(Ordering::SeqCst)
&& !disabled_temp.load(Ordering::SeqCst)
{
trace!("already enabled sunset back, notifying the sleeper");
disabler_notify.notify_one();
} else if disabled_gamma.load(Ordering::SeqCst)
|| disabled_temp.load(Ordering::SeqCst)
{
let seconds = config.disable_timeout;
trace!("waiting {seconds} seconds before enabling the sunset");
tokio::select! {
() = sleep(Duration::from_secs(seconds.into())) => {
trace!("disable timeout");
let status = format!(
"it's been {seconds} seconds since you disabled the sunset; enabling it back"
);
disabled_temp.store(false, Ordering::SeqCst);
disabled_gamma.store(false, Ordering::SeqCst);
notify.notify_one();
info!("{}", status);
if let NotifyState::Enabled(ref not) = notification {
let timeout = config.notification_timeout;
match not.show_notification(
"Too long without sunset",
&status,
"notification-icon",
timeout as i32,
) {
Ok(()) => {}
Err(e) => {
error!("Failed to show notification: {e:?}");
}
}
}
},
() = disabler_notify.notified() => {
trace!("disabler_notify notified, canceling the sleeper");
},
}
}
});
}
_ => {}
}
}
match line.trim() {
"disable_temp" => {
handle_flag_update(
&disabled_temp,
&disabled_gamma,
|flag| flag.store(true, Ordering::SeqCst),
|_| {},
"temp dimming is disabled",
"disable_temp dispatched",
¬ify,
&mut writer,
)
.await;
}
"disable_gamma" => {
handle_flag_update(
&disabled_temp,
&disabled_gamma,
|_| {},
|flag| flag.store(true, Ordering::SeqCst),
"gamma dimming is disabled",
"disable_gamma dispatched",
¬ify,
&mut writer,
)
.await;
}
"disable" => {
handle_flag_update(
&disabled_temp,
&disabled_gamma,
|flag| flag.store(true, Ordering::SeqCst),
|flag| flag.store(true, Ordering::SeqCst),
"dimming is disabled",
"disable dispatched",
¬ify,
&mut writer,
)
.await;
}
"enable_temp" => {
handle_flag_update(
&disabled_temp,
&disabled_gamma,
|flag| flag.store(false, Ordering::SeqCst),
|_| {},
"temp dimming is enabled",
"enable_temp dispatched",
¬ify,
&mut writer,
)
.await;
}
"enable_gamma" => {
handle_flag_update(
&disabled_temp,
&disabled_gamma,
|_| {},
|flag| flag.store(false, Ordering::SeqCst),
"gamma dimming is enabled",
"enable_gamma dispatched",
¬ify,
&mut writer,
)
.await;
}
"enable" => {
handle_flag_update(
&disabled_temp,
&disabled_gamma,
|flag| flag.store(false, Ordering::SeqCst),
|flag| flag.store(false, Ordering::SeqCst),
"dimming is enabled",
"enable dispatched",
¬ify,
&mut writer,
)
.await;
}
"toggle_temp" => {
toggle_flag!(
disabled_temp,
notify,
writer,
"temp",
"toggle_temp dispatched"
);
}
"toggle_gamma" => {
toggle_flag!(
disabled_gamma,
notify,
writer,
"gamma",
"toggle_gamma dispatched"
);
}
"toggle" => {
let new_val = !disabled_temp.load(Ordering::SeqCst)
&& !disabled_gamma.load(Ordering::SeqCst);
disabled_temp.store(new_val, Ordering::SeqCst);
disabled_gamma.store(new_val, Ordering::SeqCst);
notify.notify_one();
let status = format!(
"dimming is now {}",
if new_val { "disabled" } else { "enabled" }
);
info!("{}", status);
if let Err(e) = write_response(&mut writer, &status).await {
error!("Failed to respond over socket: {e}");
}
}
"status" => {
trace!("status dispatched");
let now = get_time();
let config = get_config().await;
let status = get_status_string(now, &config, &disabled_temp, &disabled_gamma);
info!("{}", status);
if let Err(e) = write_response(&mut writer, &status).await {
error!("Failed to respond over socket: {e}");
}
}
"reload" => {
trace!("reload dispatched");
let result = reload_config(notify.clone()).await;
let status = match result {
Ok(()) => "reloaded config".into(),
Err(e) => format!("failed to reload config: {e:?}"),
};
if let Err(e) = write_response(&mut writer, &status).await {
error!("Failed to respond over socket: {e}");
}
trace!("reload completed");
}
"status_notify" => {
trace!("status_notify dispatched");
let now = get_time();
let config = get_config().await;
let status = get_status_string(now, &config, &disabled_temp, &disabled_gamma);
let command_status = match notification {
NotifyState::Enabled(ref not) => {
let timeout = config.notification_timeout;
match not.show_notification(
"Sunsetting",
&status,
"notification-icon",
timeout as i32,
) {
Ok(()) => "notify notification sent".into(),
Err(e) => {
error!("Failed to show notification: {e:?}");
format!("Failed to show notification: {e:?}")
}
}
}
NotifyState::Disabled => "notify notification disabled".into(),
};
if let Err(e) = write_response(&mut writer, &command_status).await {
error!("Failed to respond over socket: {e}");
}
}
_ => error!("unknown command: {}", line.trim()),
}
}
}
}
async fn send_command(command: &str) -> Result<(), Box<dyn std::error::Error>> {
let mut stream = tokio::net::UnixStream::connect(get_socket_path()).await?;
let (reader, mut writer) = stream.split();
let mut lines = BufReader::new(reader).lines();
writer.write_all(command.as_bytes()).await?;
writer.shutdown().await?;
println!("{}", lines.next_line().await?.unwrap_or(String::new()));
Ok(())
}
#[tokio::main]
async fn main() {
let args: Vec<String> = std::env::args().collect();
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);
}
}
}
if args.len() >= 2 {
match args[1].as_str() {
"--version" | "-v" => {
println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
}
"help" => {
println!("Available commands:");
println!(" help - this message");
println!(" reload - reload config file");
println!(" status - print current status");
println!(" status_notify - send desktop notification with current status");
println!(" enable - enable automatic adjustments");
println!(" disable - disable automatic adjustments");
println!(" toggle - toggle automatic adjustments");
println!(" enable_temp - enable temperature adjustments");
println!(" disable_temp - disable temperature adjustments");
println!(" toggle_temp - toggle temperature adjustments");
println!(" enable_gamma - enable gamma adjustments");
println!(" disable_gamma - disable gamma adjustments");
println!(" toggle_gamma - toggle gamma adjustments");
}
cmd => {
let error_prefix = match cmd {
"reload" => Some("Failed to reload config: "),
"status_notify" => Some("Failed to send notification: "),
"status" | "toggle" | "toggle_gamma" | "toggle_temp" | "enable"
| "enable_gamma" | "enable_temp" | "disable" | "disable_gamma"
| "disable_temp" => Some("Failed to send status: "),
_ => None,
};
if std::path::Path::new(&get_socket_path()).exists() {
if let Some(prefix) = error_prefix {
if let Err(e) = send_command(cmd).await {
eprintln!("{prefix}{e}");
}
} else {
eprintln!("Unknown command: {cmd}");
}
} else {
eprintln!("Socket file not found, daemon not running, exiting.");
std::process::exit(1);
}
}
}
return;
}
info!("starting the daemon");
let disabled_temp = Arc::new(AtomicBool::new(false));
let disabled_gamma = Arc::new(AtomicBool::new(false));
let notify = Arc::new(Notify::new());
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()
};
fn check_binary(binary: &str) {
if !is_binary_available(binary) {
error!("{binary} is not available, exiting.");
std::process::exit(1);
}
}
for backend in cfg.gamma_backend.iter() {
match 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::ACPI => {}
GammaBackend::None => {}
}
}
for backend in cfg.temp_backend.iter() {
match backend {
TempBackend::Hyprctl => check_binary("hyprctl"),
TempBackend::Gammastep => check_binary("gammastep"),
TempBackend::Xsct => check_binary("xsct"),
TempBackend::Redshift => check_binary("redshift"),
TempBackend::None => {}
}
}
if cfg.temp_backend.iter().all(|b| *b == TempBackend::None)
&& cfg.gamma_backend.iter().all(|b| *b == GammaBackend::None)
{
error!("No backends selected, exiting.");
std::process::exit(1);
}
CONFIG.set(Arc::new(RwLock::new(cfg))).unwrap();
if std::path::Path::new(&get_socket_path()).exists() {
if std::os::unix::net::UnixStream::connect(get_socket_path()).is_ok() {
error!("Another instance is running.");
std::process::exit(1);
} else {
warn!("Stale socket found, removing.");
let _ = std::fs::remove_file(get_socket_path());
}
}
{
let disabled_temp = Arc::clone(&disabled_temp);
let disabled_gamma = Arc::clone(&disabled_gamma);
let notify = Arc::clone(¬ify);
tokio::spawn(async move {
socket_server(disabled_temp, disabled_gamma, notify).await;
});
}
{
let notify = Arc::clone(¬ify);
tokio::spawn(async move {
config_reloader(notify).await;
});
}
{
let notify = Arc::clone(¬ify);
tokio::spawn(async move {
loop {
sleep(Duration::from_secs(300)).await;
notify.notify_one();
}
});
}
let mut sigint = signal(SignalKind::interrupt()).unwrap();
let mut sigterm = signal(SignalKind::terminate()).unwrap();
{
let now = get_time();
let config = get_config().await;
let (temp, gamma) = compute_settings(now, &config);
apply_temp(temp, &config);
apply_gamma(gamma, &config);
trace!("initial settings applied: {temp}K, {gamma}%");
}
tokio::select! {
_ = async {
loop {
let now = get_time();
let config = get_config().await;
let (temp, gamma) = compute_settings(now, &config);
if disabled_temp.load(Ordering::SeqCst) {
apply_temp(
config.temp_day,
&config,
);
} else {
apply_temp(temp, &config);
}
if disabled_gamma.load(Ordering::SeqCst) {
apply_gamma(
config.gamma_day,
&config,
);
} else {
apply_gamma(gamma, &config);
}
notify.notified().await;
}} => {},
_ = sigint.recv() => {
info!("Received SIGINT, shutting down...");
},
_ = sigterm.recv() => {
info!("Received SIGTERM, shutting down...");
},
}
if std::path::Path::new(&get_socket_path()).exists() {
match std::fs::remove_file(get_socket_path()) {
Ok(()) => info!("Socket file {} removed.", get_socket_path().display()),
Err(e) => warn!(
"Failed to remove socket file {}: {e}",
get_socket_path().display()
),
}
}
}
async fn reload_config(notify: Arc<Notify>) -> Result<(), ConfigError> {
trace!("reload_config called");
let config_handle = config_handle();
let config_path = get_config_path();
match Config::load(&config_path) {
Ok(cfg) => {
debug!("Config file reloaded successfully");
fn check_binary(binary: &str) -> String {
if !is_binary_available(binary) {
error!("{binary} is not available, exiting.");
return binary.to_string();
}
String::new()
}
for backend in cfg.gamma_backend.iter().zip(cfg.temp_backend.iter()) {
let gamma_check = match backend.0 {
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::ACPI => String::new(),
GammaBackend::None => String::new(),
};
let temp_check = match backend.1 {
TempBackend::Hyprctl => check_binary("hyprctl"),
TempBackend::Gammastep => check_binary("gammastep"),
TempBackend::Xsct => check_binary("xsct"),
TempBackend::Redshift => check_binary("redshift"),
TempBackend::None => String::new(),
};
if !gamma_check.is_empty() || !temp_check.is_empty() {
error!("One or more binaries are not available, retaining old config.");
return Err(ConfigError::UnavailableCommand(format!(
"binaries {gamma_check} {temp_check} are not available"
)));
}
}
*config_handle.write().await = cfg;
reset_cache();
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);
notify.notify_one();
Ok(())
}
Err(err) => {
error!("Failed to reload config: {err:?}");
warn!("Retaining current config");
Err(err)
}
}
}
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 {
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 get_config() -> Config {
*CONFIG.get().expect("config not init").read().await
}
fn config_handle() -> Arc<RwLock<Config>> {
CONFIG.get().expect("config not init").clone()
}
fn get_time() -> OffsetDateTime {
OffsetDateTime::now_local().expect("Failed to get local time")
}