/*- * redshift-ng - Automatically adjust display colour temperature according the Sun * * Copyright (c) 2009-2018 Jon Lund Steffensen * Copyright (c) 2014-2016, 2025 Mattias Andrée * * redshift-ng is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * redshift-ng is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with redshift-ng. If not, see . */ #include "common.h" /** * The number of milliseconds to sleep normally between colour updates */ #define SLEEP_DURATION 5000U /** * The number of milliseconds to sleep between each step during * fade between large colour settings */ #define SLEEP_DURATION_SHORT 25U /** * The fade time, for when making large changes in colour * settings, divided by `SLEEP_DURATION_SHORT` */ #define FADE_LENGTH 160U char *argv0; /** * The user's current geographical location */ static struct location location; /** * Whether the location provider is available */ static int location_available; /** * State of location provider, `NULL` if none */ static LOCATION_STATE *provider_state; /** * Locaiton provider functions */ static const struct location_provider *provider; /** * State of the gamma ramp adjustment method, `NULL` if none (print mode) */ static GAMMA_STATE *method_state; /** * Gamma ramp adjustment functions */ static const struct gamma_method *method; /** * Suspend the process for a short time * * The process may be resumed earily, specifically * if it receives a signal * * @param msecs The number of milliseconds to sleep */ static void millisleep(unsigned int msecs) { #ifdef WINDOWS Sleep(msecs); /* TODO [Windows] not interruptible */ #else struct timespec ts; ts.tv_sec = (time_t)(msecs / 1000U); ts.tv_nsec = (long)(msecs % 1000U) * 1000000L; nanosleep(&ts, NULL); #endif } /** * Get the number of seconds since midnight * * @return The number of seconds since midnight */ static time_t get_time_since_midnight(void) { time_t t = time(NULL); struct tm tm; localtime_r(&t, &tm); t = (time_t)tm.tm_sec; t += (time_t)tm.tm_min * 60; t += (time_t)tm.tm_hour * 3600; return t; } /** * Print the current period of the day * * @param period The current period of the day * @param day_level The current dayness level */ static void print_period(enum period period, double day_level) { static const char *period_names[] = { /* TRANSLATORS: Name printed when period of day is unknown */ [PERIOD_NONE] = N_("None"), [PERIOD_DAYTIME] = N_("Daytime"), [PERIOD_NIGHT] = N_("Night"), [PERIOD_TRANSITION] = N_("Transition") }; if (period == PERIOD_TRANSITION) printf(_("Period: %s (%.2f%% day)\n"), gettext(period_names[period]), day_level * 100); else printf(_("Period: %s\n"), gettext(period_names[period])); } /** * Get the current period of day and the colour settings * applicable to the current time of the day * * @param colour_out Output parameter for the colour settings * @param period_out Output parameter for the period of the day * @param day_level_out Output parameter for the dayness level */ static void get_colour_settings(struct colour_setting *colour_out, enum period *period_out, double *day_level_out) { time_t time_offset; double t, elevation; /* Get dayness level */ if (scheme.type == CLOCK_SCHEME) { time_offset = get_time_since_midnight(); while (time_offset >= scheme.time.periods->next->start) scheme.time.periods = scheme.time.periods->next; time_offset -= scheme.time.periods->start; if (time_offset < 0) time_offset += ONE_DAY; t = (double)time_offset; *day_level_out = fma(t, scheme.time.periods->diff_over_duration, scheme.time.periods->day_level); } else if (scheme.type == SOLAR_SCHEME) { if (libred_solar_elevation(location.latitude, location.longitude, &elevation)) eprintf("libred_solar_elevation:"); if (verbose) { /* TRANSLATORS: Append degree symbol if possible. */ printf(_("Solar elevation: %f\n"), elevation); } *day_level_out = (elevation - scheme.elevation.low) / scheme.elevation.range; /* TODO ensure scheme.elevation.range==0 is supported */ } else { /* Static scheme, no dayness-level or peroid; day_settings == nigh_settings, use either */ *day_level_out = FNAN; *period_out = PERIOD_NONE; *colour_out = day_settings; return; } /* Clamp dayness level and get colour */ if (*day_level_out <= 0.0) { *day_level_out = 0.0; *period_out = PERIOD_NIGHT; *colour_out = night_settings; } else if (*day_level_out >= 1.0) { *day_level_out = 1.0; *period_out = PERIOD_DAYTIME; *colour_out = day_settings; } else { *period_out = PERIOD_TRANSITION; interpolate_colour_settings(&night_settings, &day_settings, *day_level_out, colour_out); } } /** * Easing function used for fade effect * * See https://github.com/mietek/ease-tween * * @param t Raw fade progress * @return Fade progress to apply */ GCC_ONLY(__attribute__((__const__))) static double ease_fade(double t) { if (t <= 0) return 0; if (t >= 1) return 1; return 1.0042954579734844 * exp(-6.4041738958415664 * exp(-7.2908241330981340 * t)); } #ifndef WINDOWS /* we don't have poll on Windows, but neither do with have any dynamic location providers */ /** * Get the current location * * The function will return once any of the following has occured: * - the specified timeout duration has elapsed, * - a location message has been received (could be location or error), or * - a signal(7) was received * * @param timeout The number of milliseconds to wait before * returning without updating the location * @param location_fd File descriptor to wait on to receive input event */ static void pull_location(unsigned int timeout, int location_fd) { struct pollfd pollfds[1]; struct location new_location; int r, new_available; /* Await new location information */ pollfds[0].fd = location_fd; pollfds[0].events = POLLIN; r = poll(pollfds, 1, (int)timeout); if (r < 0) { #ifndef WINDOWS if (errno == EINTR) return; #endif weprintf("poll:"); eprintf(_("Unable to get location from provider.")); } else if (!r) { return; } /* Get new location and availability information */ if (provider->fetch(provider_state, &new_location, &new_available) < 0) eprintf(_("Unable to get location from provider.")); if (new_available < location_available) { weprintf(_("Location is temporarily unavailable; using previous" " location until it becomes available...")); location_available = 0; return; } /* Store and announce new location */ if (new_available > location_available || !exact_eq(new_location.latitude, location.latitude) || !exact_eq(new_location.longitude, location.longitude)) { location_available = 1; location = new_location; print_location(&location); if (!location_is_valid(&location)) eprintf(_("Invalid location returned from provider.")); } } #endif /** * Loop for `PROGRAM_MODE_CONTINUAL` */ static void run_continual_mode(void) { enum period period, prev_period = PERIOD_NONE; double day_level = FNAN, prev_day_level = FNAN; int disabled = disable, prev_disabled = !disable; int prev_use_fade = !use_fade; int prev_preserve_gamma = !preserve_gamma; int done = 0; struct colour_setting colour; struct colour_setting target_colour, prev_target_colour; struct colour_setting fade_start_colour; unsigned int fade_length = 0; unsigned int fade_time = 0; unsigned int delay; double fade_progress, eased_fade_progress; #ifndef WINDOWS int location_fd; sigset_t sigusr2_mask, old_mask; enum signals commands; sigemptyset(&sigusr2_mask); sigaddset(&sigusr2_mask, SIGUSR2); #endif disable = 0; prev_target_colour = COLOUR_SETTING_NEUTRAL; colour = COLOUR_SETTING_NEUTRAL; for (;;) { /* Act on signals */ if (disable && !done) { disabled ^= 1; disable = 0; } if (exiting) { disabled = 1; exiting = 0; if (done || (disabled && !fade_length)) break; /* On second signal stop the ongoing fade */ done = 1; } #ifndef WINDOWS while (signals) { if (sigprocmask(SIG_BLOCK, &sigusr2_mask, &old_mask)) eprintf("sigprocmask:"); commands = signals; signals = 0; if (commands & SIGNAL_ORDER_BARRIER) sigdelset(&old_mask, SIGUSR2); if (commands & SIGNAL_DISABLE) disabled = 1; if (commands & SIGNAL_ENABLE) disabled = 0; if (commands & SIGNAL_RELOAD) {} /* TODO */ if (commands & SIGNAL_USE_FADE_OFF) use_fade = 0; if (commands & SIGNAL_USE_FADE_ON) use_fade = 1; if (commands & SIGNAL_PRESERVE_GAMMA_OFF) preserve_gamma = 0; if (commands & SIGNAL_PRESERVE_GAMMA_ON) preserve_gamma = 1; if (commands & SIGNAL_EXIT_WITHOUT_RESET) {} /* TODO */ if (commands & SIGNAL_VERBOSE_ON) verbose |= 2; if (commands & SIGNAL_VERBOSE_OFF) verbose &= ~2; if (commands & SIGNAL_IGNORE_SIGPIPE) if (signal(SIGPIPE, SIG_IGN) == SIG_ERR) weprintf("signal SIGPIPE SIG_IGN:"); if (sigprocmask(SIG_SETMASK, &old_mask, NULL)) eprintf("sigprocmask:"); # if defined(__linux__) if (commands & SIGNAL_REEXEC) { /* TODO */ } # endif } #endif if (verbose) { if (disabled != prev_disabled) printf(_("Status: %s\n"), disabled ? _("Disabled") : _("Enabled")); if (use_fade != prev_use_fade) printf(_("Fade: %s\n"), use_fade ? _("Disabled") : _("Enabled")); if (preserve_gamma != prev_preserve_gamma) printf(_("Preserve gamma: %s\n"), use_fade ? _("Disabled") : _("Enabled")); } prev_disabled = disabled; prev_use_fade = use_fade; prev_preserve_gamma = preserve_gamma; /* Get dayness level and corresponding colour settings */ if (disabled) { period = PERIOD_NONE; target_colour = COLOUR_SETTING_NEUTRAL; } else { get_colour_settings(&target_colour, &period, &day_level); } if (verbose && (period != prev_period || !exact_eq(day_level, prev_day_level))) print_period(period, day_level); if (period != prev_period) run_period_change_hooks(prev_period, period); prev_period = period; prev_day_level = day_level; if (verbose) { if (prev_target_colour.temperature != target_colour.temperature) printf(_("Color temperature: %luK\n"), target_colour.temperature); if (!exact_eq(prev_target_colour.brightness, target_colour.brightness)) printf(_("Brightness: %.2f\n"), target_colour.brightness); if (memcmp(prev_target_colour.gamma, target_colour.gamma, sizeof(target_colour.gamma))) { printf(_("Gamma: %.3f, %.3f, %.3f\n"), target_colour.gamma[0], target_colour.gamma[1], target_colour.gamma[2]); } } /* Fade if the parameter differences are too big to apply instantly */ if (use_fade && colour_setting_diff_is_major(&target_colour, fade_length ? &prev_target_colour : &colour)) { fade_length = FADE_LENGTH; fade_time = 0; fade_start_colour = colour; } if (fade_length) { fade_progress = ++fade_time / (double)fade_length; eased_fade_progress = ease_fade(fade_progress); interpolate_colour_settings(&fade_start_colour, &target_colour, eased_fade_progress, &colour); if (fade_time == fade_length) { fade_time = 0; fade_length = 0; } } else { colour = target_colour; } prev_target_colour = target_colour; /* Break loop when done and final fade is over */ if (done && !fade_length) break; /* Adjust temperature and sleep */ if (method->apply(method_state, &colour, preserve_gamma) < 0) eprintf(_("Temperature adjustment failed.")); delay = fade_length ? SLEEP_DURATION_SHORT : SLEEP_DURATION; #ifndef WINDOWS location_fd = scheme.type == SOLAR_SCHEME ? provider->get_fd(provider_state) : -1; if (location_fd >= 0) pull_location(delay, location_fd); else #endif millisleep(delay); /* SLEEP_DURATION_SHORT is short enough for remaining time after interruption to be ignored */ } method->restore(method_state); } int main(int argc, char *argv[]) { struct settings settings; double day_level; enum period period; struct colour_setting colour; #ifndef WINDOWS int fd; #endif argv0 = argv[0]; /* Set up localisation */ #ifdef ENABLE_NLS setlocale(LC_CTYPE, ""); setlocale(LC_MESSAGES, ""); bindtextdomain(PACKAGE, LOCALEDIR); textdomain(PACKAGE); #endif /* Ensure standard file descriptors exist */ #ifndef WINDOWS fd = open("/dev/null", O_RDWR); while (fd < 2) { if (fd < 0) eprintf("open /dev/null O_RDWR:"); fd = dup(fd); } if (fd > 2) close(fd); #endif /* Set up interprocess communication */ install_signal_handlers(); /* Get configurations and configure */ load_settings(&settings, argc, argv); if (scheme.type == SOLAR_SCHEME) { acquire_location_provider(&settings, &provider_state); provider = settings.provider; } if (mode != PROGRAM_MODE_PRINT) { acquire_adjustment_method(&settings, &method_state); method = settings.method; } config_ini_free(&settings.config); /* Get location if required */ if (scheme.type == SOLAR_SCHEME) { if (provider->get_fd(provider_state) >= 0) weprintf(_("Waiting for current location to become available...")); if (get_location(provider, provider_state, -1, &location) < 0) eprintf(_("Unable to get location from provider.")); if (!location_is_valid(&location)) eprintf(_("Invalid location returned from provider.")); print_location(&location); location_available = 1; } /* Get and print colour to set or if continual mode the initial colour */ get_colour_settings(&colour, &period, &day_level); /* needed in contiual mode for `period` and `day_level` */ if (mode == PROGRAM_MODE_CONTINUAL) colour = COLOUR_SETTING_NEUTRAL; if (verbose || mode == PROGRAM_MODE_PRINT) { if (scheme.type != STATIC_SCHEME) print_period(period, day_level); printf(_("Color temperature: %luK\n"), colour.temperature); printf(_("Brightness: %.2f\n"), colour.brightness); printf(_("Gamma: %.3f, %.3f, %.3f\n"), colour.gamma[0], colour.gamma[1], colour.gamma[2]); } switch (mode) { case PROGRAM_MODE_PRINT: break; case PROGRAM_MODE_ONE_SHOT: case PROGRAM_MODE_UNTIL_DEATH: case PROGRAM_MODE_RESET: if (method->apply(method_state, &colour, preserve_gamma) < 0) eprintf(_("Temperature adjustment failed.")); if (mode == PROGRAM_MODE_UNTIL_DEATH || method->autoreset) { weprintf(_("Press ctrl-c to stop...")); while (!exiting) { pause(); if (signals & SIGNAL_EXIT_WITHOUT_RESET) { /* TODO disable reset if if using coopgamma */ goto out; } } /* TODO reset if not using coopgamma */ } break; case PROGRAM_MODE_CONTINUAL: run_continual_mode(); break; #if defined(__GNUC__) default: __builtin_unreachable(); #endif } out: if (provider_state) provider->free(provider_state); if (method_state) method->free(method_state); free(hook_file); return 0; }