aboutsummaryrefslogtreecommitdiffstats
path: root/redshift/redshift.c
diff options
context:
space:
mode:
authorMattias Andrée <m@maandree.se>2025-03-27 18:36:26 +0100
committerMattias Andrée <m@maandree.se>2025-03-27 18:36:26 +0100
commit037b945a9f253b97faffc02d8475574e75203516 (patch)
treeb008e7d77e9daaeaaa8e7854728d715df5aafb77 /redshift/redshift.c
parenttodo list housekeeping (diff)
downloadredshift-ng-037b945a9f253b97faffc02d8475574e75203516.tar.gz
redshift-ng-037b945a9f253b97faffc02d8475574e75203516.tar.bz2
redshift-ng-037b945a9f253b97faffc02d8475574e75203516.tar.xz
one dir per subproject
Signed-off-by: Mattias Andrée <m@maandree.se>
Diffstat (limited to 'redshift/redshift.c')
-rw-r--r--redshift/redshift.c543
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;
+}