diff options
Diffstat (limited to 'redshift/redshift.c')
-rw-r--r-- | redshift/redshift.c | 543 |
1 files changed, 543 insertions, 0 deletions
diff --git a/redshift/redshift.c b/redshift/redshift.c new file mode 100644 index 0000000..5bc172e --- /dev/null +++ b/redshift/redshift.c @@ -0,0 +1,543 @@ +/*- + * redshift-ng - Automatically adjust display colour temperature according the Sun + * + * Copyright (c) 2009-2018 Jon Lund Steffensen <jonlst@gmail.com> + * Copyright (c) 2014-2016, 2025 Mattias Andrée <m@maandree.se> + * + * 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 <http://www.gnu.org/licenses/>. + */ +#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; +} |