Signal handling configured to revert to default color temp on exit or crash

This commit is contained in:
2025-10-06 22:15:33 -05:00
committed by Jason Ross
parent 70fea045d2
commit e36c2f5bfa
4 changed files with 209 additions and 42 deletions
+1
View File
@@ -17,3 +17,4 @@ toml = "0.8"
dirs = "5.0"
lazy_static = "1.5"
dialoguer = "0.11"
signal-hook = "0.3"
+54
View File
@@ -0,0 +1,54 @@
/* gamma_guard.rs -- Gamma restoration guard for cleanup
* This module provides a RAII guard that ensures gamma is restored
* even if the program crashes or panics.
*/
use crate::gamma::GammaMethod;
use crate::types::ColorSetting;
/* Guard that restores gamma to neutral (6500K) on drop.
* This ensures cleanup happens on normal exit, panic, or signal. */
pub struct GammaRestoreGuard<'a> {
gamma_method: &'a mut dyn GammaMethod,
restore_on_drop: bool,
}
impl<'a> GammaRestoreGuard<'a> {
/* Create a new gamma restore guard.
* The gamma will be restored when this guard is dropped. */
pub fn new(gamma_method: &'a mut dyn GammaMethod) -> Self {
GammaRestoreGuard {
gamma_method,
restore_on_drop: true,
}
}
/* Disable automatic restoration.
* Call this if you want to keep the current gamma on exit. */
#[allow(dead_code)]
pub fn disable_restore(&mut self) {
self.restore_on_drop = false;
}
/* Get mutable reference to the gamma method.
* This allows using the gamma method while the guard is active. */
pub fn get_mut(&mut self) -> &mut dyn GammaMethod {
self.gamma_method
}
}
impl<'a> Drop for GammaRestoreGuard<'a> {
fn drop(&mut self) {
if self.restore_on_drop {
/* Restore to neutral temperature (6500K) */
let neutral = ColorSetting {
temperature: 6500,
brightness: 1.0,
gamma: [1.0, 1.0, 1.0],
};
/* Ignore errors during cleanup - we're likely shutting down anyway */
let _ = self.gamma_method.set_temperature(&neutral, false);
}
}
}
+104 -42
View File
@@ -2,15 +2,18 @@ mod cities;
mod colorramp;
mod config;
mod gamma;
mod gamma_guard;
mod gamma_randr;
mod interactive;
mod location;
mod signals;
mod solar;
mod types;
use clap::{Parser, ValueEnum};
use config::{Config, LocationSource};
use gamma::{DummyGammaMethod, GammaMethod};
use gamma_guard::GammaRestoreGuard;
use gamma_randr::RandrGammaMethod;
use location::{GeoClue2LocationProvider, LocationProvider, ManualLocationProvider};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
@@ -329,6 +332,9 @@ fn try_geoclue2(verbose: bool) -> Result<Location, String> {
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse();
/* Install signal handlers for graceful shutdown and mode toggling */
signals::install_handlers()?;
/* Validate temperature bounds */
if args.temp_day < MIN_TEMP || args.temp_day > MAX_TEMP {
eprintln!(
@@ -392,19 +398,24 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
return Ok(());
}
/* Create gamma restore guard to ensure cleanup on exit or panic */
let mut gamma_guard = GammaRestoreGuard::new(gamma_method.as_mut());
/* Apply color temperature */
if args.verbose {
println!("Period: {}", period.name());
}
gamma_method.set_temperature(&color_setting, false)?;
gamma_guard.get_mut().set_temperature(&color_setting, false)?;
if args.one_shot {
/* For one-shot mode, don't restore gamma on exit */
gamma_guard.disable_restore();
return Ok(());
}
/* Continual mode - continuously adjust color temperature */
run_continual_mode(&location, &scheme, gamma_method.as_mut(), args.verbose)?;
run_continual_mode(&location, &scheme, &mut gamma_guard, args.verbose)?;
Ok(())
}
@@ -412,11 +423,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
/* Run continual mode loop.
This is the main loop of the continual mode which keeps track of the
current time and continuously updates the screen to the appropriate
color temperature. */
color temperature. Also handles signals for toggling and clean exit. */
fn run_continual_mode(
location: &Location,
scheme: &TransitionScheme,
gamma_method: &mut dyn GammaMethod,
gamma_guard: &mut GammaRestoreGuard,
verbose: bool,
) -> Result<(), Box<dyn std::error::Error>> {
/* Fade parameters */
@@ -430,6 +441,11 @@ fn run_continual_mode(
let mut prev_target_interp = ColorSetting::default();
let mut interp = ColorSetting::default();
/* State for signal handling */
let mut disabled = false;
let mut prev_disabled = true; /* Start as true to trigger initial status print */
let mut done = false; /* Set to true when starting shutdown fade */
if verbose {
println!("Color temperature: {}K", interp.temperature);
println!("Brightness: {:.2}", interp.brightness);
@@ -437,44 +453,84 @@ fn run_continual_mode(
/* Continuously adjust color temperature */
loop {
/* Get current time */
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs_f64();
/* Current angular elevation of the sun */
let elevation = solar::solar_elevation(now, location.lat as f64, location.lon as f64);
/* Determine period and transition progress */
let period = if elevation >= scheme.high {
Period::Daytime
} else if elevation <= scheme.low {
Period::Night
} else {
Period::Transition
};
let transition_prog = get_transition_progress_from_elevation(scheme, elevation);
/* Use transition progress to get target color temperature */
let mut target_interp = ColorSetting::default();
interpolate_transition_scheme(scheme, transition_prog, &mut target_interp);
/* Print period if it changed during this update,
or if we are in the transition period. In transition we
print the progress, so we always print it in that case. */
if verbose && (period != prev_period || period == Period::Transition) {
match period {
Period::Transition => {
println!("Period: Transition ({:.1}%)", transition_prog * 100.0);
}
_ => {
println!("Period: {}", period.name());
}
/* Check for toggle signal (SIGUSR1) */
if signals::check_toggle() && !done {
disabled = !disabled;
if verbose {
println!("Status: {}", if disabled { "Disabled" } else { "Enabled" });
}
}
/* Check for exit signal (SIGINT/SIGTERM) */
if signals::is_exiting() {
if done {
/* Second signal during fade - stop immediately */
break;
} else {
/* First signal - start shutdown fade */
done = true;
disabled = true;
signals::clear_exiting();
}
}
/* Print status change */
if verbose && disabled != prev_disabled {
println!("Status: {}", if disabled { "Disabled" } else { "Enabled" });
}
prev_disabled = disabled;
/* When disabled, use neutral temperature; otherwise calculate from solar position */
let mut target_interp = if disabled {
/* Neutral temperature (6500K) when disabled */
ColorSetting {
temperature: 6500,
brightness: 1.0,
gamma: [1.0, 1.0, 1.0],
}
} else {
/* Get current time */
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs_f64();
/* Current angular elevation of the sun */
let elevation = solar::solar_elevation(now, location.lat as f64, location.lon as f64);
/* Determine period and transition progress */
let period = if elevation >= scheme.high {
Period::Daytime
} else if elevation <= scheme.low {
Period::Night
} else {
Period::Transition
};
let transition_prog = get_transition_progress_from_elevation(scheme, elevation);
/* Use transition progress to get target color temperature */
let mut temp_interp = ColorSetting::default();
interpolate_transition_scheme(scheme, transition_prog, &mut temp_interp);
/* Print period if it changed during this update,
or if we are in the transition period. In transition we
print the progress, so we always print it in that case. */
if verbose && (period != prev_period || period == Period::Transition) {
match period {
Period::Transition => {
println!("Period: Transition ({:.1}%)", transition_prog * 100.0);
}
_ => {
println!("Period: {}", period.name());
}
}
}
prev_period = period;
temp_interp
};
/* Start fade if the parameter differences are too big to apply instantly. */
if (fade_length == 0 && color_setting_diff_is_major(&interp, &target_interp))
|| (fade_length != 0 && color_setting_diff_is_major(&target_interp, &prev_target_interp))
@@ -510,12 +566,16 @@ fn run_continual_mode(
}
/* Adjust temperature */
gamma_method.set_temperature(&interp, false)?;
gamma_guard.get_mut().set_temperature(&interp, false)?;
/* Save period and target color setting as previous */
prev_period = period;
/* Save target color setting as previous */
prev_target_interp = target_interp;
/* If shutdown was requested and fade is complete, exit */
if done && fade_length == 0 {
break;
}
/* Sleep length depends on whether a fade is ongoing. */
let delay = if fade_length != 0 {
SLEEP_DURATION_SHORT
@@ -525,4 +585,6 @@ fn run_continual_mode(
std::thread::sleep(Duration::from_millis(delay));
}
Ok(())
}
+50
View File
@@ -0,0 +1,50 @@
/* signals.rs -- Signal handling for redshift
* This file provides signal handlers for graceful shutdown and toggling modes.
*
* Signals handled:
* - SIGUSR1: Toggle between enabled/disabled state (restores gamma when disabled)
* - SIGINT/SIGTERM: Clean shutdown with gamma restoration
*/
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
/* Global atomic flags for signal state.
* These are safe to access from signal handlers and main thread. */
lazy_static::lazy_static! {
static ref EXITING: Arc<AtomicBool> = Arc::new(AtomicBool::new(false));
static ref TOGGLE_REQUESTED: Arc<AtomicBool> = Arc::new(AtomicBool::new(false));
}
/* Install signal handlers.
* Returns an error if signal handler registration fails. */
pub fn install_handlers() -> Result<(), Box<dyn std::error::Error>> {
use signal_hook::consts::signal::*;
use signal_hook::flag;
/* SIGINT and SIGTERM set the exiting flag */
flag::register(SIGINT, Arc::clone(&EXITING))?;
flag::register(SIGTERM, Arc::clone(&EXITING))?;
/* SIGUSR1 sets the toggle flag */
flag::register(SIGUSR1, Arc::clone(&TOGGLE_REQUESTED))?;
Ok(())
}
/* Check if an exit signal (SIGINT or SIGTERM) was received.
* This should be called from the main loop. */
pub fn is_exiting() -> bool {
EXITING.load(Ordering::Relaxed)
}
/* Check if a toggle signal (SIGUSR1) was received.
* This returns true only once per signal, then clears the flag. */
pub fn check_toggle() -> bool {
TOGGLE_REQUESTED.swap(false, Ordering::Relaxed)
}
/* Clear the exiting flag. Used after starting shutdown fade. */
pub fn clear_exiting() {
EXITING.store(false, Ordering::Relaxed);
}