aboutsummaryrefslogtreecommitdiffstats
path: root/redshift
diff options
context:
space:
mode:
Diffstat (limited to 'redshift')
-rw-r--r--redshift/Makefile66
-rw-r--r--redshift/arg.h379
-rw-r--r--redshift/backend-direct.c941
-rw-r--r--redshift/colour.c97
-rw-r--r--redshift/common.h1690
-rw-r--r--redshift/config-ini.c267
-rw-r--r--redshift/config.c1172
-rw-r--r--redshift/config.mk20
-rw-r--r--redshift/gamma-coopgamma.c535
-rw-r--r--redshift/gamma-drm.c51
-rw-r--r--redshift/gamma-dummy.c89
-rw-r--r--redshift/gamma-quartz.c51
-rw-r--r--redshift/gamma-randr.c51
-rw-r--r--redshift/gamma-vidmode.c51
-rw-r--r--redshift/gamma-wingdi.c51
-rw-r--r--redshift/gamma.c139
-rw-r--r--redshift/hooks.c249
-rw-r--r--redshift/location-corelocation.m311
-rw-r--r--redshift/location-geoclue2.c451
-rw-r--r--redshift/location-geofile.c114
-rw-r--r--redshift/location-manual.c118
-rw-r--r--redshift/location-timezone.c108
-rw-r--r--redshift/location.c249
-rw-r--r--redshift/redshift.11379
-rw-r--r--redshift/redshift.c543
-rw-r--r--redshift/signals.c209
-rw-r--r--redshift/util.c317
-rw-r--r--redshift/windows/appicon.rc1
-rw-r--r--redshift/windows/redshift.icobin0 -> 87891 bytes
-rw-r--r--redshift/windows/versioninfo.rc20
30 files changed, 9719 insertions, 0 deletions
diff --git a/redshift/Makefile b/redshift/Makefile
new file mode 100644
index 0000000..417d001
--- /dev/null
+++ b/redshift/Makefile
@@ -0,0 +1,66 @@
+.POSIX:
+
+CONFIGFILE = config.mk
+include $(CONFIGFILE)
+
+
+VERSION_STRING = redshift-ng 1.13
+
+
+OBJ =\
+ backend-direct.o\
+ colour.o\
+ config.o\
+ config-ini.o\
+ gamma.o\
+ gamma-coopgamma.o\
+ gamma-drm.o\
+ gamma-dummy.o\
+ gamma-quartz.o\
+ gamma-randr.o\
+ gamma-vidmode.o\
+ gamma-wingdi.o\
+ hooks.o\
+ location.o\
+ location-geoclue2.o\
+ location-manual.o\
+ location-geofile.o\
+ location-timezone.o\
+ redshift.o\
+ signals.o\
+ util.o
+
+
+CPPFLAGS_STRINGS =\
+ -D'PACKAGE="$(PACKAGE)"'\
+ -D'VERSION_STRING="$(VERSION_STRING)"'\
+ -D'LOCALEDIR="$(LOCALEDIR)"'
+
+
+all: redshift
+$(OBJ): common.h arg.h
+
+.c.o:
+ $(CC) -c -o $@ $< $(CFLAGS) $(CPPFLAGS) $(CPPFLAGS_STRINGS)
+
+redshift: $(OBJ)
+ $(CC) -o $@ $(OBJ) $(LDFLAGS)
+
+install: redshift
+ mkdir -p -- "$(DESTDIR)$(PREFIX)/bin"
+ mkdir -p -- "$(DESTDIR)$(MANPREFIX)/man1/"
+ cp -- redshift "$(DESTDIR)$(PREFIX)/bin/"
+ cp -- redshift.1 "$(DESTDIR)$(MANPREFIX)/man1/"
+
+uninstall:
+ -rm -f -- "$(DESTDIR)$(PREFIX)/bin/redshift"
+ -rm -f -- "$(DESTDIR)$(MANPREFIX)/man1/redshift.1"
+
+clean:
+ -rm -f -- *.o *.a *.lo *.su
+ -rm -f -- redshift
+
+.SUFFIXES:
+.SUFFIXES: .o .c
+
+.PHONY: all install uninstall clean
diff --git a/redshift/arg.h b/redshift/arg.h
new file mode 100644
index 0000000..2ff6dfc
--- /dev/null
+++ b/redshift/arg.h
@@ -0,0 +1,379 @@
+/*-
+ * This file is taken, with some parts removed, from libsimple
+ *
+ * ISC License
+ *
+ * © 2017, 2018, 2021, 2022, 2023, 2024, 2025 Mattias Andrée <m@maandree.se>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+
+/**
+ * The zeroth command line argument, the name of the process,
+ * set by the command line parsing macros
+ */
+extern char *argv0;
+
+
+/**
+ * Map from a long option to a short option
+ *
+ * NB! Long options with optional arguments should
+ * have to map entries, one where `.long_flag` ends
+ * with '=' and `.with_arg` is non-zero, and one
+ * where `.long_flag` does not end with '=' and
+ * `.with_arg` is zero. These *cannot* have the same
+ * `.short_flag`
+ */
+struct longopt {
+ /**
+ * The long option, if the value must be attached
+ * to the flag, this must end with '='
+ */
+ const char *long_flag;
+
+ /**
+ * The equivalent short option
+ *
+ * The first symbol in the short option
+ * (normally '-') will be `.long_flag[0]`
+ */
+ char short_flag;
+
+#if defined(__clang__)
+# pragma clang diagnostic push
+# pragma clang diagnostic ignored "-Wpadded"
+#endif
+
+ /**
+ * Whether the option takes an argument
+ */
+ int with_arg;
+
+#if defined(__clang__)
+# pragma clang diagnostic pop
+#endif
+};
+
+
+/**
+ * `ARGBEGIN {} ARGEND;` creates a switch statement
+ * instead a loop that parses the command line arguments
+ * according to the POSIX specification for default
+ * behaviour (extensions of the behaviour is possible)
+ *
+ * This macro requires that the variables `argc` and
+ * `argv` are defined and that `argv[argc]` is `NULL`,
+ * `argc` shall be a non-negative `int` that tells
+ * how many elements (all non-`NULL`) are available in
+ * `argv`, the list of command line arguments
+ *
+ * When parsing stops, `argc` and `argv` are updated
+ * shuch that all parsed arguments are removed; the
+ * contents of `argv` will not be modified, rather
+ * the pointer `argv` will be updated to `&argv[n]`
+ * where `n` is the number of parsed elements in `argv`
+ *
+ * Inside `{}` in `ARGBEGIN {} ARGEND;` there user
+ * shall specify `case` statements for each recognised
+ * command line option, and `default` for unrecognised
+ * option. For example:
+ *
+ * ARGBEGIN {
+ * case 'a':
+ * // handle -a
+ * break;
+ * case 'b':
+ * // handle -b
+ * break;
+ * case ARGNUM:
+ * // handle -0, -1, -2, ..., -9
+ * break;
+ * default:
+ * // print usage information for other flags
+ * usage();
+ * } ARGEND;
+ */
+#define ARGBEGIN ARGBEGIN2(1, 0)
+
+/**
+ * `SUBARGBEGIN {} ARGEND;` is similar to
+ * `ARGBEGIN {} ARGEND;`, however, `argv0`
+ * is not set to `argv[0]`, instead `argv[0]`
+ * is handled like any other element in `argv`
+ */
+#define SUBARGBEGIN ARGBEGIN2(0, 0)
+
+/**
+ * Flexible alternative to `ARGBEGIN`
+ *
+ * @param WITH_ARGV0 If 0, behave like `SUBARGBEGIN`,
+ * otherwise, behave like `ARGBEGIN`
+ * @param KEEP_DASHDASH If and only if 0, "--" is not removed
+ * `argv` before parsing is stopped when it
+ * is encountered
+ */
+#define ARGBEGIN2(WITH_ARGV0, KEEP_DASHDASH)\
+ do {\
+ char flag_, *lflag_, *arg_;\
+ int brk_ = 0, again_;\
+ size_t i_, n_;\
+ if (WITH_ARGV0) {\
+ argv0 = *argv;\
+ argv += !!argv0;\
+ argc -= !!argv0;\
+ }\
+ (void) arg_;\
+ (void) i_;\
+ (void) n_;\
+ for (; argv[0] && argv[0][0] && argv[0][1]; argc--, argv++) {\
+ lflag_ = argv[0];\
+ if (argv[0][0] == '-') {\
+ if (argv[0][1] == '-' && !argv[0][2]) {\
+ if (!(KEEP_DASHDASH))\
+ argv++, argc--;\
+ break;\
+ }\
+ for (argv[0]++; argv[0][0]; argv[0]++) {\
+ flag_ = argv[0][0];\
+ if (flag_ == '-' && &argv[0][-1] != lflag_)\
+ usage();\
+ arg_ = argv[0][1] ? &argv[0][1] : argv[1];\
+ do {\
+ again_ = 0;\
+ switch (flag_) {
+
+/**
+ * Test multiple long options and go to
+ * corresponding short option case
+ *
+ * If the long option is found (see documentation
+ * for `struct longopt` for details), the keyword
+ * `break` is used to break the `case` (or `default`),
+ * and at the next iteration of the parsing loop, the
+ * case will be `.short_flag` for the entry where the
+ * argument matched `.long_flag` and `.with_arg`
+ *
+ * @param LONGOPTS:struct longopt * The options, list shall end
+ * with `NULL` as `.long_flag`
+ */
+#define ARGMAPLONG(LONGOPTS)\
+ for (i_ = 0; (LONGOPTS)[i_].long_flag; i_++) {\
+ if (TESTLONG((LONGOPTS)[i_].long_flag, (LONGOPTS)[i_].with_arg)) {\
+ flag_ = (LONGOPTS)[i_].short_flag;\
+ again_ = 1;\
+ break;\
+ }\
+ }\
+ if (again_)\
+ break
+
+/**
+ * Allows flags to start with another symbol than '-'
+ *
+ * Usage example:
+ * ARGBEGIN {
+ * case 'a': // handle -a
+ * break;
+ * default:
+ * usage();
+ * } ARGALT('+') {
+ * case 'a': // handle +a
+ * break;
+ * default:
+ * usage();
+ * } ARGALT('/') {
+ * case 'a': // handle /a
+ * break;
+ * default:
+ * usage();
+ * } ARGEND;
+ *
+ * @param SYMBOL:char The symbol flags should begin with
+ */
+#define ARGALT(SYMBOL)\
+ }\
+ } while (again_);\
+ if (brk_) {\
+ argc -= arg_ == argv[1];\
+ argv += arg_ == argv[1];\
+ brk_ = 0;\
+ break;\
+ }\
+ }\
+ } else if (argv[0][0] == SYMBOL) {\
+ if (argv[0][1] == SYMBOL && !argv[0][2])\
+ break;\
+ for (argv[0]++; argv[0][0]; argv[0]++) {\
+ flag_ = argv[0][0];\
+ if (flag_ == SYMBOL && &argv[0][-1] != lflag_)\
+ usage();\
+ arg_ = argv[0][1] ? &argv[0][1] : argv[1];\
+ do {\
+ again_ = 0;\
+ switch (flag_) {
+
+/**
+ * Refer to `ARGBEGIN`, `SUBARGBEGIN`, and `ARGBEGIN2`
+ */
+#define ARGEND\
+ }\
+ } while (again_);\
+ if (brk_) {\
+ argc -= arg_ == argv[1];\
+ argv += arg_ == argv[1];\
+ brk_ = 0;\
+ break;\
+ }\
+ }\
+ } else {\
+ break;\
+ }\
+ }\
+ } while (0)
+
+
+/**
+ * `case ARGNUM` creates a switch statement case for each digit
+ */
+#define ARGNUM '0': case '1': case '2': case '3': case '4':\
+ case '5': case '6': case '7': case '8': case '9'
+
+/**
+ * Get the flag character, for example in `case 'a'`,
+ * 'a' is returned
+ *
+ * @return :char The option's identifying character
+ */
+#define FLAG() (flag_)
+
+/**
+ * Get the entire argument that is being parsed
+ *
+ * Note that an argument can contain multiple options
+ * and it can contain the last options value but the
+ * value can also be in the next argument
+ *
+ * @return :char * The current command line argument
+ */
+#define LFLAG() (lflag_)
+
+/**
+ * Get the current option's value, if it
+ * does not have a value, call `usage`
+ * (which terminates the process)
+ *
+ * Using this macro lets the parser knows
+ * that the option has a value
+ *
+ * @return :char * The option's value, never `NULL`
+ */
+#define ARG() (arg_ ? (brk_ = 1, arg_) : (usage(), NULL))
+
+/**
+ * Get the current option's value, if the option
+ * does not have a value, `NULL` is returned
+ *
+ * Note that the value may appear at the next
+ * argument (next element in `argv`) which in that
+ * case is returned
+ *
+ * Using this macro lets the parser knows
+ * that the option has a value
+ *
+ * @return :char * The option's value, `NULL` if
+ * the option does not have a value
+ */
+#define ARGNULL() (arg_ ? (brk_ = 1, arg_) : NULL)
+
+/**
+ * Get the remaining part of the current command
+ * line argument (element in `argv`) — as well as
+ * the character that specifies the flag — as the
+ * value of the argument
+ *
+ * Using this macro lets the parser knows
+ * that the option has a value
+ *
+ * Usage example:
+ *
+ * char *arg;
+ * ARGBEGIN {
+ * case ARGNUM:
+ * arg = ARGHERE();
+ * // `arg` is the number after '-', for example,
+ * // if the command line contains the argument
+ * // "-12345", `arg` will be `12345`
+ * break;
+ * case 'n':
+ * arg = &ARGHERE()[1];
+ * if (*arg) {
+ * // flag 'n' has a value (`argv`)
+ * } else {
+ * // flag 'n' does not have a value
+ * }
+ * default:
+ * usage();
+ * } ARGEND;
+ *
+ * @return :char * The option's value include the flag
+ * character, never `NULL` or ""
+ */
+#define ARGHERE() (brk_ = 1, argv[0])
+
+
+/**
+ * Test if the argument is a specific long option
+ *
+ * Example:
+ *
+ * ARGBEGIN {
+ * case 'x':
+ * handle_dash_x:
+ * // handle -x
+ * break;
+ * case '-':
+ * if (TESTLONG("--xdev", 0))
+ * goto handle_dash_x;
+ * else
+ * usage();
+ * break;
+ * default:
+ * usage();
+ * } ARGEND;
+ *
+ * @param FLAG:const char * The flag, must end with '=' if the
+ * value must be attached to the flag,
+ * must not have side-effects
+ * @param WARG:int Whether the option takes an argument,
+ * should not have side-effects
+ */
+#define TESTLONG(FLG, WARG)\
+ ((WARG)\
+ ? ((!strncmp(lflag_, (FLG), (n_ = strlen(FLG), n_ -= ((FLG)[n_ - !!n_] == '='))) && lflag_[n_] == '=')\
+ ? (lflag_[n_] = '\0',\
+ (arg_ = &lflag_[n_ + 1]),\
+ (brk_ = 1))\
+ : (!strcmp(lflag_, (FLG))\
+ ? (argv[1]\
+ ? ((arg_ = argv[1]),\
+ (brk_ = 1))\
+ : (usage(), 0))\
+ : 0))\
+ : (!strcmp(lflag_, (FLG))\
+ ? (brk_ = 1)\
+ : 0))
diff --git a/redshift/backend-direct.c b/redshift/backend-direct.c
new file mode 100644
index 0000000..9f62c4d
--- /dev/null
+++ b/redshift/backend-direct.c
@@ -0,0 +1,941 @@
+/*-
+ * 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"
+
+
+/**
+ * Union of libgamma gamma ramp structures
+ */
+union gamma_ramps {
+ /**
+ * Gamma ramp sizes
+ */
+ struct {
+ size_t red; /**< Size of the gamma ramp for the red channel */
+ size_t green; /**< Size of the gamma ramp for the green channel */
+ size_t blue; /**< Size of the gamma ramp for the blue channel */
+ } size;
+
+ /* Ramp structures */
+#define X(SUFFIX, RAMPS, TYPE, MAX, DEPTH)\
+ struct libgamma_gamma_##RAMPS RAMPS
+ LIST_RAMPS_STOP_VALUE_TYPES(X, ;);
+#undef X
+};
+
+
+/**
+ * State for partition (e.g. X screen or graphics card)
+ */
+struct partition_state {
+ /**
+ * libgamma state
+ */
+ struct libgamma_partition_state state;
+
+ /**
+ * Index of CRTC when indexing spans multiple X screens
+ */
+ size_t crtc_num_offset;
+};
+
+
+/**
+ * CRTC state
+ */
+struct crtc_state {
+ /**
+ * libgamma state
+ */
+ struct libgamma_crtc_state state;
+
+ /**
+ * Gamma ramp depth, 0 if uninitialised,
+ * -1 if single-precision float,
+ * -2 if double-precision float,
+ * otherwise the number of bits (of unsigned integer)
+ */
+ signed gamma_depth;
+
+ /**
+ * Gamma ramps used by the gamma adjustment function
+ */
+ union gamma_ramps gamma_ramps;
+
+ /**
+ * Original state of gamma ramps
+ */
+ union gamma_ramps saved_gamma_ramps;
+};
+
+
+/**
+ * CRTC number, either overall number or partion and number within partition
+ */
+struct crtc_number {
+ /**
+ * The partition of the EDID, -1 if using overall ordinal
+ */
+ ssize_t partition;
+
+ /**
+ * CRTC number within the partition, or overall if no partition is specified
+ */
+ size_t crtc;
+};
+
+
+struct gamma_state {
+ /**
+ * libgamma state for the site (e.g. X display)
+ */
+ struct libgamma_site_state site;
+
+ /**
+ * Whether the adjustment method supports fetching
+ * EDIDs from the outputs
+ */
+ unsigned supports_edid : 1;
+
+ /**
+ * Whether the adjustment method supports multiple
+ * sites rather than just the default site
+ */
+ unsigned multiple_sites : 1;
+
+ /**
+ * Whether the adjustment method supports multiple partitions
+ * per site
+ */
+ unsigned multiple_partitions : 1;
+
+ /**
+ * Whether the adjustment method supports multiple CRTC:s
+ * per partition per site
+ */
+ unsigned multiple_crtcs : 1;
+
+ /**
+ * Whether partitions are graphics cards
+ */
+ unsigned partitions_are_graphics_cards : 1;
+
+ /**
+ * Whether a parition has been selected
+ */
+ unsigned partitions_selected : 1;
+
+ /**
+ * Whether CRTCs have been selected
+ */
+ unsigned crtcs_selected : 1;
+
+ /**
+ * Whether the program has connected to the site
+ */
+ unsigned connected : 1;
+
+ /**
+ * The libgamma constant for the adjustment method
+ */
+ int method;
+
+ /**
+ * Number of selected CRTCs
+ */
+ size_t ncrtcs;
+
+ /**
+ * Number of selected EDIDs
+ */
+ size_t nedids;
+
+ /**
+ * Number of selected parition
+ */
+ size_t npartitions;
+
+ /**
+ * Selected paritions (if `partitions_selected`)
+ */
+ size_t *selected_partitions;
+
+ /**
+ * Selected CRTC numbers
+ *
+ * Deallocated by when no longer needed
+ */
+ struct crtc_number *selected_crtcs;
+
+ /**
+ * Selected EDIDs
+ *
+ * Deallocated by when no longer needed
+ */
+ char **selected_edids;
+
+ /**
+ * Name of site to connect to, `NULL` if not selected
+ * or once connected to the site
+ */
+ char *site_name;
+
+ /**
+ * State for selected paritions
+ */
+ struct partition_state *partitions;
+
+ /**
+ * State for selected CRTCs
+ */
+ struct crtc_state *crtcs;
+};
+
+
+int
+direct_create(struct gamma_state **state_out, int method, const char *method_name)
+{
+ struct libgamma_method_capabilities caps;
+ struct gamma_state *state;
+ int err;
+
+ *state_out = NULL;
+
+ if (!libgamma_is_method_available(method)) {
+ weprintf(_("Adjustment method `%s' is not available"), method_name);
+ return -1;
+ }
+
+ err = libgamma_method_capabilities(&caps, sizeof(caps), method);
+ if (err) {
+ weprintf("libgamma_method_capabilities %s: %s", libgamma_const_of_method(method), libgamma_strerror(err));
+ return -1;
+ }
+
+ state = *state_out = ecalloc(1, sizeof(**state_out));
+ state->selected_crtcs = NULL;
+ state->partitions = NULL;
+ state->site_name = NULL;
+ state->method = method;
+ state->supports_edid = (caps.crtc_information & LIBGAMMA_CRTC_INFO_EDID) ? 1 : 0;
+ state->multiple_sites = caps.multiple_sites;
+ state->multiple_partitions = caps.multiple_partitions;
+ state->multiple_crtcs = caps.multiple_crtcs;
+ state->partitions_are_graphics_cards = caps.partitions_are_graphics_cards;
+
+ if (state->multiple_sites)
+ return 0;
+
+ err = libgamma_site_initialise(&state->site, method, NULL);
+ if (err) {
+ weprintf("libgamma_site_initialise %s NULL: %s", libgamma_const_of_method(method), libgamma_strerror(err));
+ free(state);
+ *state_out = NULL;
+ return -1;
+ }
+
+ return 0;
+}
+
+
+void
+direct_print_help(int method)
+{
+ struct libgamma_method_capabilities caps;
+
+ if (libgamma_method_capabilities(&caps, sizeof(caps), method)) {
+ printf(_("Adjustment method not available\n"));
+ printf("\n");
+ return;
+ }
+
+ if (caps.multiple_sites)
+ printf(" display=%s %s\n", _("NAME "), _("Display server instance to apply adjustments to"));
+
+ if (caps.multiple_partitions && caps.partitions_are_graphics_cards) {
+ /* TRANSLATORS: "N" represents "ordinal"; right-pad with spaces to preserve display width */
+ printf(" card=%s %s\n", _("N "), _("Graphics card to apply adjustments to"));
+ } else if (caps.multiple_partitions) {
+ /* TRANSLATORS: "N" represents "ordinal"; right-pad with spaces to preserve display width */
+ printf(" screen=%s %s\n", _("N "), _("List of comma-separated X screens to apply adjustments to"));
+ }
+
+ if (caps.multiple_crtcs) {
+ /* TRANSLATORS: "N" represents "number"; right-pad with spaces to preserve display width */
+ printf(" crtc=%s %s\n", _("N "), _("List of comma-separated CRTCs to apply adjustments to"));
+ }
+ if (caps.multiple_crtcs && (caps.crtc_information & LIBGAMMA_CRTC_INFO_EDID)) {
+ printf(" edid=%s %s\n", _("EDID "), _("List of comma-separated EDIDS of monitors to apply "
+ "adjustments to, enter `list' to list available monitors"));
+ }
+
+ if (caps.multiple_sites || caps.multiple_partitions || caps.multiple_crtcs)
+ printf("\n");
+}
+
+
+int
+direct_set_option(struct gamma_state *state, const char *key, const char *value)
+{
+ if (state->multiple_sites && !strcasecmp(key, "display")) {
+ return direct_set_site(state, key, value);
+ } else if (state->multiple_partitions && !strcasecmp(key, state->partitions_are_graphics_cards ? "card" : "screen")) {
+ return direct_set_partitions(state, key, value);
+ } else if (state->multiple_crtcs && !strcasecmp(key, "crtc")) {
+ return direct_set_crtcs(state, key, value);
+ } else if (state->multiple_crtcs && state->supports_edid && !strcasecmp(key, "edid")) {
+ return direct_set_edids(state, key, value);
+ } else if (!strcasecmp(key, "preserve")) {
+ weprintf(_("Deprecated method parameter ignored: `%s'."), key);
+ return 0;
+ } else {
+ weprintf(_("Unknown method parameter: `%s'."), key);
+ return -1;
+ }
+}
+
+
+int
+direct_set_site(struct gamma_state *state, const char *key, const char *value)
+{
+ if (state->site_name) {
+ weprintf(_("`%s' option specified multiple times, using last selection."), key);
+ free(state->site_name);
+ }
+ state->site_name = estrdup(value);
+ return 0;
+}
+
+
+int
+direct_set_partitions(struct gamma_state *state, const char *key, const char *value)
+{
+ const char *end, *p;
+ uintmax_t num;
+ size_t count;
+
+ /* Check previously unspecified */
+ if (state->partitions_selected)
+ weprintf(_("`%s' option specified multiple times, using last selection."), key);
+ state->partitions_selected = 1;
+
+ /* Check if all are selected */
+ state->npartitions = 0;
+ free(state->selected_partitions);
+ state->selected_partitions = NULL;
+ if (!*value || !strcasecmp(value, "all"))
+ return 0;
+
+ /* Get number count */
+ for (p = value, count = 1; *p; p++)
+ if (*p == ',')
+ count++;
+ state->selected_partitions = ecalloc(count, sizeof(*state->selected_partitions));
+
+ /* Parse numbers */
+ errno = 0;
+ for (p = value; *p; p = end) {
+ num = strtoumax(p, (void *)&end, 10);
+ state->selected_partitions[state->npartitions++] = (size_t)num;
+ if (num > (uintmax_t)SIZE_MAX || (*end && *end != ',') || !isdigit(*p) || errno) {
+ weprintf(_("Invalid value of `%s' option: `%s'."), key, value);
+ return -1;
+ }
+ end = &end[*end == ','];
+ }
+
+ return 0;
+}
+
+
+int
+direct_set_crtcs(struct gamma_state *state, const char *key, const char *value)
+{
+ const char *end, *p;
+ uintmax_t num;
+ size_t count;
+
+ /* Check previously unspecified */
+ if (state->crtcs_selected)
+ weprintf(_("`%s' option specified multiple times, using last selection."), key);
+ state->crtcs_selected = 1;
+
+ /* Check if all are selected */
+ state->ncrtcs = 0;
+ free(state->selected_crtcs);
+ state->selected_crtcs = NULL;
+ if (!*value || !strcasecmp(value, "all"))
+ return 0;
+
+ /* Get number count */
+ for (p = value, count = 1; *p; p++)
+ if (*p == ',')
+ count++;
+ state->selected_crtcs = ecalloc(count, sizeof(*state->selected_crtcs));
+
+ /* Parse numbers */
+ errno = 0;
+ for (p = value; *p; p = end) {
+ num = strtoumax(p, (void *)&end, 10);
+ state->selected_crtcs[state->ncrtcs].partition = -1;
+ state->selected_crtcs[state->ncrtcs].crtc = (size_t)num;
+ if (num > (uintmax_t)SIZE_MAX || (*end && *end != ',' && *end != '.') || !isdigit(*p) || errno) {
+ invalid:
+ weprintf(_("Invalid value of `%s' option: `%s'."), key, value);
+ return -1;
+ }
+ if (*end == '.') {
+ if (!state->multiple_partitions || num > (uintmax_t)SSIZE_MAX)
+ goto invalid;
+ state->selected_crtcs[state->ncrtcs].partition = (ssize_t)num;
+ p = &end[1];
+ num = strtoumax(p, (void *)&end, 10);
+ state->selected_crtcs[state->ncrtcs].crtc = (size_t)num;
+ if (num > (uintmax_t)SIZE_MAX || (*end && *end != ',') || !isdigit(*p) || errno)
+ goto invalid;
+ }
+ end = &end[*end == ','];
+ state->ncrtcs++;
+ }
+
+ return 0;
+}
+
+
+int
+direct_set_edids(GAMMA_STATE *state, const char *key, const char *value)
+{
+ const char *end, *p;
+ size_t count, len;
+
+ /* Check previously unspecified */
+ if (state->nedids) {
+ weprintf(_("`%s' option specified multiple times, using last selection."), key);
+ while (state->nedids)
+ free(state->selected_edids[--state->nedids]);
+ free(state->selected_edids);
+ state->selected_edids = NULL;
+ state->nedids = 0;
+ }
+
+ /* Get number count */
+ for (p = value, count = 1; *p; p++)
+ if (*p == ',')
+ count++;
+ state->selected_edids = ecalloc(count, sizeof(*state->selected_edids));
+
+ /* Split */
+ for (p = value;; p = end) {
+ end = strchr(p, ',');
+ if (!end) {
+ state->selected_edids[state->nedids++] = estrdup(p);
+ break;
+ }
+ len = (size_t)(end++ - p);
+ state->selected_edids[state->nedids] = emalloc(len + 1U);
+ memcpy(state->selected_edids[state->nedids], p, len);
+ state->selected_edids[state->nedids][len] = '\0';
+ state->nedids++;
+ }
+
+ return 0;
+}
+
+
+int
+direct_start(struct gamma_state *state)
+{
+ struct {
+ size_t partition;
+ size_t crtc;
+ char *edid;
+ } *resolved_edids = NULL;
+ struct libgamma_crtc_information crtc_info;
+ struct libgamma_crtc_state crtc_state;
+ size_t crtc_num_offset = 0;
+ size_t crtcs_removed = 0;
+ size_t nresolved_edids = 0;
+ size_t i, j, k, num, part, count;
+ int err;
+
+ /* Connect to display server */
+ if (state->multiple_sites) {
+ if (state->site_name && !*state->site_name) {
+ free(state->site_name);
+ state->site_name = NULL;
+ }
+ err = libgamma_site_initialise(&state->site, state->method, state->site_name);
+ state->site_name = NULL;
+ if (err) {
+ weprintf("libgamma_site_initialise %s %s: %s",
+ libgamma_const_of_method(state->method),
+ state->site_name ? state->site_name : "NULL",
+ libgamma_strerror(err));
+ return -1;
+ }
+ state->connected = 1;
+ }
+
+ /* Allocate partition states */
+ if (state->npartitions) {
+ state->partitions = ecalloc(state->npartitions, sizeof(*state->partitions));
+ for (i = 0; i < state->npartitions; i++) {
+ state->partitions[i].state.partition = state->selected_partitions[i];
+ state->partitions[i].state.data = NULL;
+ }
+ free(state->selected_partitions);
+ state->selected_partitions = NULL;
+ } else if (!state->site.partitions_available) {
+ if (state->partitions_are_graphics_cards)
+ weprintf(_("No graphics card found."));
+ else
+ weprintf(_("No X screen found."));
+ return -1;
+ } else {
+ state->npartitions = state->site.partitions_available;
+ state->partitions = ecalloc(state->npartitions, sizeof(*state->partitions));
+ for (i = 0; i < state->npartitions; i++) {
+ state->partitions[i].state.partition = i;
+ state->partitions[i].state.data = NULL;
+ }
+ }
+
+ /* Map the partition indices in CRTC numbers from indices of selected
+ * partitions, and allocate partitions specified via CRTC numbers */
+ for (i = 0; i < state->ncrtcs; i++) {
+ if (state->selected_crtcs[i].partition < 0)
+ continue;
+ for (j = 0; j < state->npartitions; j++) {
+ if ((size_t)state->selected_crtcs[i].partition == state->partitions[j].state.partition) {
+ state->selected_crtcs[i].partition = (ssize_t)j;
+ break;
+ }
+ }
+ if (j == state->npartitions) {
+ state->partitions = realloc(state->partitions, (state->npartitions + 1U) * sizeof(*state->partitions));
+ state->partitions[state->npartitions].state.partition = (size_t)state->selected_crtcs[i].partition;
+ state->partitions[state->npartitions].state.data = NULL;
+ state->npartitions++;
+ }
+ }
+
+ /* Initialise partitions */
+ for (i = 0; i < state->npartitions; i++) {
+ num = state->partitions[i].state.partition;
+ err = libgamma_partition_initialise(&state->partitions[i].state, &state->site, num);
+ if (err) {
+ weprintf("libgamma_partition_initialise %zu: %s", num, libgamma_strerror(err));
+ return -1;
+ }
+ state->partitions[i].crtc_num_offset = crtc_num_offset;
+ crtc_num_offset += state->partitions[i].state.crtcs_available;
+ }
+
+ /* Resolve EDIDs */
+ if (state->nedids) {
+ for (i = 0; i < state->npartitions; i++) {
+ count = state->partitions[i].state.crtcs_available;
+ if (!count)
+ continue;
+ resolved_edids = erealloc(resolved_edids, (nresolved_edids + count) + sizeof(*resolved_edids));
+ for (j = 0; j < count; j++) {
+ if (libgamma_crtc_initialise(&crtc_state, &state->partitions[i].state, j))
+ continue;
+ libgamma_get_crtc_information(&crtc_info, sizeof(crtc_info), &crtc_state, LIBGAMMA_CRTC_INFO_EDID);
+ if (crtc_info.edid_error)
+ goto next_crtc;
+ resolved_edids[nresolved_edids].partition = i;
+ resolved_edids[nresolved_edids].crtc = j;
+ resolved_edids[nresolved_edids].edid = libgamma_behex_edid(crtc_info.edid, crtc_info.edid_length);
+ if (!resolved_edids[nresolved_edids].edid)
+ eprintf("libgamma_behex_edid:");
+ nresolved_edids++;
+ next_crtc:
+ libgamma_crtc_information_destroy(&crtc_info);
+ libgamma_crtc_destroy(&crtc_state);
+ }
+ }
+ for (i = 0; i < state->nedids; i++) {
+ if (!strcasecmp(state->selected_edids[i], "list")) {
+ printf(_("Available outputs:\n"));
+ for (i = 0; i < nresolved_edids; i++)
+ printf(" %s\n", resolved_edids[i].edid);
+ direct_free(state);
+ exit(0);
+ }
+ for (j = 0; j < nresolved_edids; j++) {
+ if (!strcasecmp(state->selected_edids[i], resolved_edids[i].edid)) {
+ if (!state->multiple_partitions) {
+ weprintf("Resolved output `%s' to CRTC %zu",
+ resolved_edids[i].edid, resolved_edids[i].crtc);
+ } else if (state->partitions_are_graphics_cards) {
+ weprintf("Resolved output `%s' to CRTC %zu on graphics card %zu",
+ resolved_edids[i].edid, resolved_edids[i].crtc,
+ resolved_edids[i].partition);
+ } else {
+ weprintf("Resolved output `%s' to CRTC %zu on X screen %zu",
+ resolved_edids[i].edid, resolved_edids[i].crtc,
+ resolved_edids[i].partition);
+ }
+ count = state->ncrtcs + 1U;
+ state->selected_crtcs = erealloc(state->selected_crtcs, count * sizeof(*state->crtcs));
+ state->selected_crtcs[state->ncrtcs].partition = (ssize_t)resolved_edids[i].partition;
+ state->selected_crtcs[state->ncrtcs].crtc = resolved_edids[i].crtc;
+ state->ncrtcs++;
+ goto next_edid;
+ }
+ }
+ weprintf(_("Output `%s' found."), state->selected_edids[i]);
+ next_edid:
+ free(state->selected_edids[i]);
+ state->selected_edids[i] = NULL;
+ }
+ while (nresolved_edids)
+ free(resolved_edids[--nresolved_edids].edid);
+ free(resolved_edids);
+ free(state->selected_edids);
+ state->selected_edids = NULL;
+ }
+
+ /* Allocate CRTCs states and map to partition–CRTC pairs */
+ if (state->ncrtcs) {
+ state->crtcs = ecalloc(state->ncrtcs, sizeof(*state->crtcs));
+ for (i = 0; i < state->ncrtcs; i++) {
+ state->crtcs[i].state.data = NULL;
+ state->crtcs[i].state.partition = NULL;
+ if (state->selected_crtcs[i].partition >= 0)
+ state->crtcs[i].state.partition = &state->partitions[state->selected_crtcs[i].partition].state;
+ state->crtcs[i].state.crtc = state->selected_crtcs[i].crtc;
+ }
+ for (i = 0; i < state->ncrtcs; i++) {
+ if (state->crtcs[i].state.partition)
+ continue;
+ for (j = 1; j < state->npartitions; j++)
+ if (state->crtcs[i].state.crtc < state->partitions[j].crtc_num_offset)
+ break;
+ state->crtcs[i].state.partition = &state->partitions[j - 1U].state;
+ state->crtcs[i].state.crtc -= state->partitions[j - 1U].crtc_num_offset;
+ }
+ } else if (!crtc_num_offset) {
+ weprintf(_("No CRTCs found."));
+ return -1;
+ } else {
+ state->ncrtcs = crtc_num_offset;
+ state->crtcs = ecalloc(state->ncrtcs, sizeof(*state->crtcs));
+ for (j = 0, i = 0; j < state->npartitions; j++) {
+ for (k = 0; k < state->partitions[j].state.crtcs_available; k++, i++) {
+ state->crtcs[i].state.data = NULL;
+ state->crtcs[i].state.partition = &state->partitions[j].state;
+ state->crtcs[i].state.crtc = k;
+ }
+ }
+ }
+
+ /* Initialise CRTCs and fetch original gamma adjustments */
+ for (i = 0; i < state->ncrtcs; i++) {
+ /* Get CRTC */
+ part = state->crtcs[i].state.partition->partition;
+ num = state->crtcs[i].state.crtc;
+ err = libgamma_crtc_initialise(&state->crtcs[i].state, state->crtcs[i].state.partition, num);
+ if (err) {
+ weprintf("libgamma_crtc_initialise %zu %zu: %s", part, num, libgamma_strerror(err));
+ return -1;
+ }
+
+ /* Get gamma ramp inforamtion for CRTC */
+ libgamma_get_crtc_information(&crtc_info, sizeof(crtc_info), &state->crtcs[i].state,
+ LIBGAMMA_CRTC_INFO_GAMMA_SIZE |
+ LIBGAMMA_CRTC_INFO_GAMMA_DEPTH |
+ LIBGAMMA_CRTC_INFO_GAMMA_SUPPORT);
+ if (crtc_info.gamma_size_error || crtc_info.gamma_depth_error) {
+ libgamma_crtc_destroy(&state->crtcs[i].state);
+ state->crtcs[i].state.data = NULL;
+ crtcs_removed++;
+ goto no_gamma_support;
+ }
+ if (!crtc_info.gamma_support_error && crtc_info.gamma_support == LIBGAMMA_NO) {
+ no_gamma_support:
+ if (state->multiple_crtcs) {
+ if (!state->multiple_partitions)
+ weprintf(_("Adjustments are not supported on CRTC %zu."), num);
+ else if (state->partitions_are_graphics_cards)
+ weprintf(_("Adjustments are not supported on CRTC %zu on graphics card %zu."), num, part);
+ else
+ weprintf(_("Adjustments are not supported on CRTC %zu on X screen %zu."), num, part);
+ } else {
+ if (!state->multiple_partitions)
+ weprintf(_("Adjustments are not supported."));
+ else if (state->partitions_are_graphics_cards)
+ weprintf(_("Adjustments are not supported on graphics card %zu."), part);
+ else
+ weprintf(_("Adjustments are not supported on X screen %zu."), part);
+ }
+ if (!state->crtcs[i].state.data)
+ goto next;
+ }
+
+ /* Initialise and fetch gamma adjustments */
+ state->crtcs[i].gamma_ramps.size.red = crtc_info.red_gamma_size;
+ state->crtcs[i].gamma_ramps.size.green = crtc_info.green_gamma_size;
+ state->crtcs[i].gamma_ramps.size.blue = crtc_info.blue_gamma_size;
+ state->crtcs[i].saved_gamma_ramps.size.red = crtc_info.red_gamma_size;
+ state->crtcs[i].saved_gamma_ramps.size.green = crtc_info.green_gamma_size;
+ state->crtcs[i].saved_gamma_ramps.size.blue = crtc_info.blue_gamma_size;
+ switch (crtc_info.gamma_depth) {
+#define X(SUFFIX, RAMPS, TYPE, MAX, DEPTH)\
+ case DEPTH:\
+ if (libgamma_gamma_##RAMPS##_initialise(&state->crtcs[i].gamma_ramps.RAMPS) ||\
+ libgamma_gamma_##RAMPS##_initialise(&state->crtcs[i].saved_gamma_ramps.RAMPS))\
+ eprintf("libgamma_gamma_"#RAMPS"_initialise:");\
+ j = 0;\
+ do {\
+ err = libgamma_crtc_get_gamma_##RAMPS(&state->crtcs[i].state,\
+ &state->crtcs[i].saved_gamma_ramps.RAMPS);\
+ } while (err && j++ < 10);\
+ if (!err) {\
+ state->crtcs[i].gamma_depth = DEPTH;\
+ break;\
+ }\
+ if (state->multiple_crtcs) {\
+ if (!state->multiple_partitions)\
+ weprintf(_("Could not get current adjustments for CRTC %zu."), num);\
+ else if (state->partitions_are_graphics_cards)\
+ weprintf(_("Could not get current adjustments for CRTC %zu on graphics card %zu."),\
+ num, part);\
+ else\
+ weprintf(_("Could not get current adjustments for CRTC %zu on X screen %zu."), num, part);\
+ } else {\
+ if (!state->multiple_partitions)\
+ weprintf(_("Could not get current adjustments."));\
+ else if (state->partitions_are_graphics_cards)\
+ weprintf(_("Could not get current adjustments for graphics card %zu."), part);\
+ else\
+ weprintf(_("Could not get current adjustments for X screen %zu."), part);\
+ }\
+ libgamma_gamma_##RAMPS##_destroy(&state->crtcs[i].gamma_ramps.RAMPS);\
+ libgamma_gamma_##RAMPS##_destroy(&state->crtcs[i].saved_gamma_ramps.RAMPS);\
+ libgamma_crtc_destroy(&state->crtcs[i].state);\
+ state->crtcs[i].state.data = NULL;\
+ crtcs_removed++;\
+ goto next
+
+ LIST_RAMPS_STOP_VALUE_TYPES(X, ;);
+#undef X
+
+ default:
+ if (state->multiple_crtcs) {
+ if (!state->multiple_partitions)
+ weprintf(_("Unsupported gamma ramp type on CRTC %zu."), num);
+ else if (state->partitions_are_graphics_cards)
+ weprintf(_("Unsupported gamma ramp type on CRTC %zu on graphics card %zu."), num, part);
+ else
+ weprintf(_("Unsupported gamma ramp type on CRTC %zu on X screen %zu."), num, part);
+ } else {
+ if (!state->multiple_partitions)
+ weprintf(_("Unsupported gamma ramp type."));
+ else if (state->partitions_are_graphics_cards)
+ weprintf(_("Unsupported gamma ramp type on graphics card %zu."), part);
+ else
+ weprintf(_("Unsupported gamma ramp type on X screen %zu."), part);
+ }
+ return -1;
+ }
+
+ next:
+ libgamma_crtc_information_destroy(&crtc_info);
+ }
+
+ /* Unlist removed CRTCs */
+ if (crtcs_removed) {
+ num = state->ncrtcs - crtcs_removed;
+ for (i = j = 0; crtcs_removed; i++, j++)
+ if (!state->crtcs[i].state.data)
+ break;
+ for (; crtcs_removed; i++) {
+ if (state->crtcs[i].state.data)
+ crtcs_removed--;
+ else
+ memmove(&state->crtcs[j++], &state->crtcs[i], sizeof(*state->crtcs));
+ }
+ memmove(&state->crtcs[j], &state->crtcs[i], (state->ncrtcs - i) * sizeof(*state->crtcs));
+ state->ncrtcs = num;
+ }
+ if (!state->ncrtcs) {
+ weprintf(_("No usable CRTCs available."));
+ return -1;
+ }
+
+ free(state->selected_crtcs);
+ state->selected_crtcs = NULL;
+ return 0;
+}
+
+
+int
+direct_apply(struct gamma_state *state, const struct colour_setting *setting, int preserve)
+{
+ size_t i, err_count = 0, crtc, part;
+ const char *errstr;
+ int err;
+
+ for (i = 0; i < state->ncrtcs; i++) {
+ switch (state->crtcs[i].gamma_depth) {
+#define X(SUFFIX, RAMPS, TYPE, MAX, DEPTH)\
+ case DEPTH:\
+ fill_ramps_##SUFFIX(state->crtcs[i].gamma_ramps.RAMPS.red,\
+ state->crtcs[i].gamma_ramps.RAMPS.green,\
+ state->crtcs[i].gamma_ramps.RAMPS.blue,\
+ preserve ? state->crtcs[i].saved_gamma_ramps.RAMPS.red : NULL,\
+ preserve ? state->crtcs[i].saved_gamma_ramps.RAMPS.green : NULL,\
+ preserve ? state->crtcs[i].saved_gamma_ramps.RAMPS.blue : NULL,\
+ state->crtcs[i].gamma_ramps.size.red,\
+ state->crtcs[i].gamma_ramps.size.green,\
+ state->crtcs[i].gamma_ramps.size.blue,\
+ setting);\
+ err = libgamma_crtc_set_gamma_##RAMPS(&state->crtcs[i].state, &state->crtcs[i].gamma_ramps.RAMPS);\
+ break
+
+ LIST_RAMPS_STOP_VALUE_TYPES(X, ;);
+#undef X
+
+ default:
+ err_count++;
+ err = 0;
+ break;
+ }
+
+ if (err) {
+ err_count++;
+ crtc = state->crtcs[i].state.crtc;
+ part = state->crtcs[i].state.partition->partition;
+ errstr = libgamma_strerror(err);
+ if (state->multiple_crtcs) {
+ if (!state->multiple_partitions)
+ weprintf(_("Unable to set adjustments for CRTC %zu: %s."), crtc, errstr);
+ else if (state->partitions_are_graphics_cards)
+ weprintf(_("Unable to set adjustments for CRTC %zu on graphics card %zu: %s."),
+ crtc, part, errstr);
+ else
+ weprintf(_("Unable to set adjustments for CRTC %zu on X screen %zu: %s."),
+ crtc, part, errstr);
+ } else {
+ if (!state->multiple_partitions)
+ weprintf(_("Unable to set adjustments: %s."), errstr);
+ else if (state->partitions_are_graphics_cards)
+ weprintf(_("Unable to set adjustments for graphics card %zu: %s."), part, errstr);
+ else
+ weprintf(_("Unable to set adjustments for X screen %zu: %s."), part, errstr);
+ }
+ }
+ }
+
+ return err_count == state->ncrtcs ? -1 : 0;
+}
+
+
+void
+direct_restore(struct gamma_state *state)
+{
+ size_t i, crtc, part;
+ const char *errstr;
+ int err;
+
+ for (i = 0; i < state->ncrtcs; i++) {
+ switch (state->crtcs[i].gamma_depth) {
+#define X(SUFFIX, RAMPS, TYPE, MAX, DEPTH)\
+ case DEPTH:\
+ err = libgamma_crtc_set_gamma_##RAMPS(&state->crtcs[i].state, &state->crtcs[i].saved_gamma_ramps.RAMPS);\
+ break
+
+ LIST_RAMPS_STOP_VALUE_TYPES(X, ;);
+#undef X
+
+ default:
+ err = 0;
+ break;
+ }
+
+ if (err) {
+ crtc = state->crtcs[i].state.crtc;
+ part = state->crtcs[i].state.partition->partition;
+ errstr = libgamma_strerror(err);
+ if (state->multiple_crtcs) {
+ if (!state->multiple_partitions)
+ weprintf(_("Unable to restore adjustments for CRTC %zu: %s."), crtc, errstr);
+ else if (state->partitions_are_graphics_cards)
+ weprintf(_("Unable to restore adjustments for CRTC %zu on graphics card %zu: %s."),
+ crtc, part, errstr);
+ else
+ weprintf(_("Unable to restore adjustments for CRTC %zu on X screen %zu: %s."),
+ crtc, part, errstr);
+ } else {
+ if (!state->multiple_partitions)
+ weprintf(_("Unable to restore adjustments: %s."), errstr);
+ else if (state->partitions_are_graphics_cards)
+ weprintf(_("Unable to restore adjustments for graphics card %zu: %s."), part, errstr);
+ else
+ weprintf(_("Unable to restore adjustments for X screen %zu: %s."), part, errstr);
+ }
+ }
+ }
+}
+
+
+void
+direct_free(struct gamma_state *state)
+{
+ size_t i;
+ if (state->crtcs) {
+ for (i = 0; i < state->ncrtcs; i++) {
+ switch (state->crtcs[i].gamma_depth) {
+#define X(SUFFIX, RAMPS, TYPE, MAX, DEPTH)\
+ case DEPTH:\
+ libgamma_gamma_##RAMPS##_destroy(&state->crtcs[i].gamma_ramps.RAMPS);\
+ libgamma_gamma_##RAMPS##_destroy(&state->crtcs[i].saved_gamma_ramps.RAMPS);\
+ break
+ LIST_RAMPS_STOP_VALUE_TYPES(X, ;);
+#undef X
+ default:
+ case 0: /* not initialised */
+ break;
+ }
+ if (state->crtcs[i].state.data)
+ libgamma_crtc_destroy(&state->crtcs[i].state);
+ }
+ free(state->crtcs);
+ }
+ if (state->partitions) {
+ for (i = 0; i < state->npartitions; i++)
+ if (state->partitions[i].state.data)
+ libgamma_partition_destroy(&state->partitions[i].state);
+ free(state->partitions);
+ }
+ free(state->selected_partitions);
+ free(state->selected_crtcs);
+ if (state->selected_edids) {
+ for (i = 0; i < state->nedids; i++)
+ free(state->selected_edids[i]);
+ free(state->selected_edids);
+ }
+ if (state->connected)
+ libgamma_site_destroy(&state->site);
+ free(state->site_name);
+ free(state);
+}
diff --git a/redshift/colour.c b/redshift/colour.c
new file mode 100644
index 0000000..4d85cb5
--- /dev/null
+++ b/redshift/colour.c
@@ -0,0 +1,97 @@
+/*-
+ * 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"
+
+
+void
+interpolate_colour_settings(const struct colour_setting *a, const struct colour_setting *b,
+ double t, struct colour_setting *result)
+{
+ int i;
+ t = CLAMP(0.0, t, 1.0);
+ result->temperature = (1.0 - t) * a->temperature + t * b->temperature;
+ result->brightness = (1.0 - t) * a->brightness + t * b->brightness;
+ for (i = 0; i < 3; i++)
+ result->gamma[i] = (1.0 - t) * a->gamma[i] + t * b->gamma[i];
+}
+
+
+int
+colour_setting_diff_is_major(const struct colour_setting *a, const struct colour_setting *b)
+{
+ return MAX(a->temperature, b->temperature) - MIN(a->temperature, b->temperature) > 25UL ||
+ fabs(a->brightness - b->brightness) > 0.1 ||
+ fabs(a->gamma[0] - b->gamma[0]) > 0.1 ||
+ fabs(a->gamma[1] - b->gamma[1]) > 0.1 ||
+ fabs(a->gamma[2] - b->gamma[2]) > 0.1;
+}
+
+
+#define X(SUFFIX, RAMPS, TYPE, MAX, DEPTH)\
+ /**
+ * Fill a gamma ramp
+ *
+ * @param ramp The gamma ramp
+ * @param saved Saved gamma ramp with calibrations to preserver, or `NULL`
+ * @param size The gamma ramp size (number of stops)
+ * @param brightness The brightness (between 0 and 1) of the channel, which is
+ * the overall applied brightness multiplied but the effect
+ * on the channel from the colour temperature
+ * @param gamma The gamma to apply to the channel
+ */\
+ static void\
+ fill_ramp_##SUFFIX(TYPE *ramp, const TYPE *saved, size_t size, double brightness, double gamma)\
+ {\
+ size_t i;\
+ double v;\
+ brightness /= (size - 1U);\
+ if (exact_eq(gamma, 1.0)) {\
+ brightness *= (MAX);\
+ for (i = 0; i < size; i++)\
+ ramp[i] = (TYPE)(i * brightness);\
+ } else {\
+ gamma = 1.0 / gamma;\
+ for (i = 0; i < size; i++) {\
+ v = pow(i * brightness, gamma) * (MAX);\
+ ramp[i] = (TYPE)v;\
+ }\
+ }\
+ if (saved) {\
+ for (i = 0; i < size; i++) {\
+ v = (double)ramp[i] / (MAX) * (size - 1U);\
+ ramp[i] = saved[(size_t)v];\
+ }\
+ }\
+ }\
+ \
+ void\
+ fill_ramps_##SUFFIX(TYPE *gamma_r, TYPE *gamma_g, TYPE *gamma_b,\
+ const TYPE *saved_r, const TYPE *saved_g, const TYPE *saved_b,\
+ size_t size_r, size_t size_g, size_t size_b,\
+ const struct colour_setting *setting)\
+ {\
+ double r = 1, g = 1, b = 1;\
+ libred_get_colour(setting->temperature, &r, &g, &b);\
+ fill_ramp_##SUFFIX(gamma_r, saved_r, size_r, setting->brightness * r, setting->gamma[0]);\
+ fill_ramp_##SUFFIX(gamma_g, saved_g, size_g, setting->brightness * g, setting->gamma[1]);\
+ fill_ramp_##SUFFIX(gamma_b, saved_b, size_b, setting->brightness * b, setting->gamma[2]);\
+ }
+
+LIST_RAMPS_STOP_VALUE_TYPES(X,)
diff --git a/redshift/common.h b/redshift/common.h
new file mode 100644
index 0000000..d9a268d
--- /dev/null
+++ b/redshift/common.h
@@ -0,0 +1,1690 @@
+/*-
+ * 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/>.
+ */
+#ifndef WINDOWS
+# if defined(__WIN32__) || defined(_WIN32)
+# define WINDOWS
+# endif
+#endif
+
+
+#include "arg.h"
+
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <ctype.h>
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <locale.h>
+#include <math.h>
+#include <signal.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <time.h>
+#include <unistd.h>
+#ifdef _POSIX_TIMERS
+# include <sys/time.h>
+#endif
+#ifdef WINDOWS
+# include <windows.h>
+# define localtime_r(T, TM) localtime_s((TM), (T))
+# define pause() millisleep(100U)
+#else
+# include <sys/file.h>
+# include <poll.h>
+# include <pwd.h>
+# include <time.h>
+#endif
+
+#include <libgamma.h>
+#include <libgeome.h>
+#include <libred.h>
+
+
+#ifdef ENABLE_NLS
+# include <libintl.h>
+#else
+# define gettext(s) s
+#endif
+
+/**
+ * List for translation, and translate in place
+ *
+ * @param s:string-literal Translatable string
+ * @return :const char * Translation of `s`
+ */
+#define _(s) gettext(s)
+
+/**
+ * List for translation without translating in place
+ *
+ * @param s:string-literal Translatable string
+ * @return :string-literal `s` as is
+ */
+#define N_(s) s
+
+
+#if defined(__clang__)
+# pragma clang diagnostic ignored "-Wunsafe-buffer-usage" /* broken in clang 19.1.7 */
+# pragma clang diagnostic ignored "-Wdisabled-macro-expansion" /* warns about system headers (also a stupid warning) */
+# pragma clang diagnostic ignored "-Wassign-enum" /* warns about bit field enums */
+# pragma clang diagnostic ignored "-Wpadded" /* only relevant for library headers */
+# pragma clang diagnostic ignored "-Wcomma" /* comma is useful in loop conditions */
+# pragma clang diagnostic ignored "-Wcovered-switch-default" /* stupid warning: not necessary true */
+#elif defined(__GNUC__)
+# pragma GCC diagnostic ignored "-Wunsuffixed-float-constants" /* stupid warning */
+# pragma GCC diagnostic ignored "-Wpadded" /* only relevant for library headers */
+#endif
+
+
+#if defined(__GNUC__)
+# define GCC_ONLY(...) __VA_ARGS__
+#else
+# define GCC_ONLY(...)
+#endif
+
+
+/**
+ * Truncate a value into a closed range
+ *
+ * @param LO The lower bound
+ * @param X The value to truncated
+ * @param UP The upper bound
+ * @return `X` truncated such that it is at least `LO` and at most `UP`
+ */
+#define CLAMP(LO, X, UP) (((LO) > (X)) ? (LO) : (((X) < (UP)) ? (X) : (UP)))
+
+/**
+ * Check whether a value is within a closed range
+ *
+ * @param LO The lower bound
+ * @param X The value to check
+ * @param UP The upper bound
+ * @return :int 1 if `X` is within [`LO`, `UP`], 0 otherwise
+ */
+#define WITHIN(LO, X, UP) ((LO) <= (X) && (X) <= (UP))
+
+/**
+ * Check whether a value is within a bounded, open range
+ *
+ * @param LO The lower bound
+ * @param X The value to check
+ * @param UP The upper bound
+ * @return :int 1 if `X` is within (`LO`, `UP`), 0 otherwise
+ */
+#define BETWEEN(LO, X, UP) ((LO) < (X) && (X) < (UP))
+
+/**
+ * Quiet not-a-number `double`
+ */
+#define FNAN ((double)NAN) /* because clang warns when implicitly promoted to double */
+
+/**
+ * Get the number of elements in an array
+ *
+ * @param ARR The array, must not be a pointer
+ * @return :size_t The number of elements in `ARR` (constant
+ * expression, unless its size is dynamic)
+ */
+#define ELEMSOF(ARR) (sizeof(ARR) / (sizeof(*(ARR))))
+
+/**
+ * Get the smallest of two numerical values
+ *
+ * @param A One of the values
+ * @param B The other value
+ * @return The smallest of `A` and `B`
+ */
+#define MIN(A, B) ((A) < (B) ? (A) : (B))
+
+/**
+ * Get the largest of two numerical values
+ *
+ * @param A One of the values
+ * @param B The other value
+ * @return The largest of `A` and `B`
+ */
+#define MAX(A, B) ((A) > (B) ? (A) : (B))
+
+
+/**
+ * Symbol used to delimit paths in environment
+ * variables listing multiple paths
+ */
+#ifdef WINDOWS
+# define PATH_DELIMITER ';'
+#else
+# define PATH_DELIMITER ':'
+#endif
+
+/**
+ * The number of seconds in a day
+ */
+#define ONE_DAY ((time_t)(24L * 60L * 60L))
+
+
+/**
+ * Minimum valid latitude
+ */
+#define MIN_LATITUDE -90.0
+
+/**
+ * Maximum valid latitude
+ */
+#define MAX_LATITUDE 90.0
+
+/**
+ * Minimum valid longitude
+ */
+#define MIN_LONGITUDE -180.0
+
+/**
+ * Maximum valid longitude
+ */
+#define MAX_LONGITUDE 180.0
+
+/**
+ * Minimum allowed colour temperature
+ */
+#define MIN_TEMPERATURE ((unsigned long int)LIBRED_LOWEST_TEMPERATURE)
+
+/**
+ * Maximum allowed colour temperature
+ */
+#define MAX_TEMPERATURE ULONG_MAX
+
+/**
+ * Minimum allowed whitepoint brightness
+ */
+#define MIN_BRIGHTNESS 0.1
+
+/**
+ * Maximum allowed whitepoint brightness
+ */
+#define MAX_BRIGHTNESS 1.0
+
+/**
+ * Minimum allowed gamma
+ */
+#define MIN_GAMMA 0.1
+
+/**
+ * Maximum allowed gamma
+ */
+#define MAX_GAMMA 10.0
+
+
+/**
+ * The colour temperature corresponding to no effect
+ */
+#define NEUTRAL_TEMPERATURE 6500UL
+
+/**
+ * The whitepoint brightness corresponding to
+ * full brightness (no effect)
+ */
+#define NEUTRAL_BRIGHTNESS 1.0
+
+/**
+ * The gamma corresponding to no effect (linear output level curve)
+ */
+#define NEUTRAL_GAMMA 1.0
+
+
+/**
+ * Default daytime colour temperature
+ */
+#define DEFAULT_DAY_TEMPERATURE 6500UL
+
+/**
+ * Default night colour temperature
+ */
+#define DEFAULT_NIGHT_TEMPERATURE 4500UL
+
+/**
+ * Default daytime whitepoint brightness level
+ */
+#define DEFAULT_DAY_BRIGHTNESS NEUTRAL_BRIGHTNESS
+
+/**
+ * Default night whitepoint brightness level
+ */
+#define DEFAULT_NIGHT_BRIGHTNESS NEUTRAL_BRIGHTNESS
+
+/**
+ * Default daytime gamma value
+ */
+#define DEFAULT_DAY_GAMMA NEUTRAL_GAMMA
+
+/**
+ * Default night gamma value
+ */
+#define DEFAULT_NIGHT_GAMMA NEUTRAL_GAMMA
+
+/**
+ * The solar elevation, in degrees, that marks the
+ * threshold to daytime
+ */
+#define DEFAULT_HIGH_ELEVATION 3.0
+
+/**
+ * The solar elevation, in degrees, that marks the
+ * threshold to night
+ */
+#define DEFAULT_LOW_ELEVATION LIBRED_SOLAR_ELEVATION_CIVIL_DUSK_DAWN
+
+
+/**
+ * Initialiser for `struct colour_setting`
+ *
+ * Sets all values to their neutral values (no effects applied)
+ */
+#define COLOUR_SETTING_NEUTRAL\
+ ((struct colour_setting){\
+ NEUTRAL_TEMPERATURE,\
+ NEUTRAL_BRIGHTNESS,\
+ {NEUTRAL_GAMMA, NEUTRAL_GAMMA, NEUTRAL_GAMMA}\
+ })
+
+
+/**
+ * State of an adjustment method
+ *
+ * Each method has their own definition of this structure
+ */
+typedef struct gamma_state GAMMA_STATE;
+
+/**
+ * State of a location provider
+ *
+ * Each provider has their own definition of this structure
+ */
+typedef struct location_state LOCATION_STATE;
+
+
+/**
+ * The time of the day: day, night, or twilight
+ */
+enum period {
+ /**
+ * None applied
+ */
+ PERIOD_NONE,
+
+ /**
+ * Full daytime
+ */
+ PERIOD_DAYTIME,
+
+ /**
+ * Full nighttime
+ */
+ PERIOD_NIGHT,
+
+ /**
+ * Transitioning between day and night
+ * (either direction) (twilight)
+ */
+ PERIOD_TRANSITION
+};
+
+
+/**
+ * The mode the program is running in
+ */
+enum program_mode {
+ /**
+ * Run in foreground continually update temperature
+ */
+ PROGRAM_MODE_CONTINUAL,
+
+ /**
+ * Update temperature once then exit
+ */
+ PROGRAM_MODE_ONE_SHOT,
+
+ /**
+ * Update temperature once and reset when killed
+ */
+ PROGRAM_MODE_UNTIL_DEATH,
+
+ /**
+ * Print setting and exit
+ */
+ PROGRAM_MODE_PRINT,
+
+ /**
+ * Remove effects and exit
+ */
+ PROGRAM_MODE_RESET
+};
+
+
+/**
+ * By what the effects of the application change
+ */
+enum scheme_type {
+ /**
+ * Effects are dependent on the Sun's elevation
+ */
+ SOLAR_SCHEME,
+
+ /**
+ * Effects are dependent on the wall clock time
+ */
+ CLOCK_SCHEME,
+
+ /**
+ * Effects do not change
+ */
+ STATIC_SCHEME
+};
+
+/**
+ * Scheme has not been specified
+ */
+#define UNSPECIFIED_SCHEME STATIC_SCHEME
+
+
+/**
+ * The sources where an setting was been loaded from
+ *
+ * Higher valued sources have higher priority
+ *
+ * This is a bitmask `enum`
+ */
+enum setting_source {
+ /**
+ * No setting loaded, default value set
+ */
+ SETTING_DEFAULT = 0x00,
+
+ /**
+ * Setting loaded from configuration file
+ */
+ SETTING_CONFIGFILE = 0x01,
+
+ /**
+ * Setting loaded from command line arguments
+ */
+ SETTING_CMDLINE = 0x02
+};
+
+
+/**
+ * SIGUSR2 signal values as bitmask values
+ */
+enum signals {
+ /**
+ * Block SIGUSR2 until all signals have been processed
+ */
+ SIGNAL_ORDER_BARRIER = 1 << 0,
+
+ /**
+ * Disable the effects of redshift
+ */
+ SIGNAL_DISABLE = 1 << 1,
+
+ /**
+ * Enable the effects of redshift
+ */
+ SIGNAL_ENABLE = 1 << 2,
+
+ /**
+ * Reload the configuration file
+ *
+ * Settings from the command line will be overriden
+ */
+ SIGNAL_RELOAD = 1 << 3,
+
+ /**
+ * Execute into the currently installed version of redshift
+ */
+ SIGNAL_REEXEC = 1 << 4,
+
+ /**
+ * Set the "fade" setting to off
+ */
+ SIGNAL_USE_FADE_OFF = 1 << 5,
+
+ /**
+ * Set the "fade" setting to on
+ */
+ SIGNAL_USE_FADE_ON = 1 << 6,
+
+ /**
+ * Set the "preserve-gamma" setting to off
+ */
+ SIGNAL_PRESERVE_GAMMA_OFF = 1 << 7,
+
+ /**
+ * Set the "preserve-gamma" setting to on
+ */
+ SIGNAL_PRESERVE_GAMMA_ON = 1 << 8,
+
+ /**
+ * Exit the process without removing the its effects
+ *
+ * If the used adjustment method does not support
+ * leaving the effects, they will be removed
+ */
+ SIGNAL_EXIT_WITHOUT_RESET = 1 << 9,
+
+ /**
+ * Do not terminate redshift the standard output
+ * and standard error are closed
+ */
+ SIGNAL_IGNORE_SIGPIPE = 1 << 10,
+
+ /**
+ * Enable verbose mode
+ */
+ SIGNAL_VERBOSE_ON = 1 << 11,
+
+ /**
+ * Disable verbose mode
+ *
+ * Ignore if started in verbose mode (-v option)
+ */
+ SIGNAL_VERBOSE_OFF = 1 << 12
+};
+
+
+/**
+ * Specification for a path that consists of a two parts:
+ * the first being defined by the environment, and the
+ * seocnd being a static string
+ */
+struct env_path {
+ /**
+ * Whether the environment variable referenced by `.prefix`
+ * should be split at each colon (:) into multiple paths to
+ * test
+ *
+ * On Windows semicolon (;) is used instead of colon
+ */
+ int multidir_env;
+
+ /**
+ * Environment variable to use as the first part of the path
+ *
+ * `NULL` if the user's home directory should be used
+ *
+ * The empty string if `.suffix` should be used as is
+ */
+ const char *prefix_env;
+
+ /**
+ * The second part of the path
+ */
+ const char *suffix;
+};
+
+
+/**
+ * Geographical location, using GPS coordinates
+ */
+struct location {
+ /**
+ * Degrees north of the equator
+ */
+ double latitude;
+
+ /**
+ * Degrees east of the prime meridian
+ */
+ double longitude;
+};
+
+
+/**
+ * Colour setting to apply
+ */
+struct colour_setting {
+ /**
+ * Colour temperature, in Kelvin
+ */
+ unsigned long int temperature;
+
+ /**
+ * Whitepoint brightness level
+ */
+ double brightness;
+
+ /**
+ * Gamma correct, for the each RGB channel
+ * in the order: red, green, and blue
+ */
+ double gamma[3];
+};
+
+
+/**
+ * Linked list of time periods for `CLOCK_SCHEME`
+ */
+struct time_period {
+ /**
+ * The number of seconds after midnight the period starts
+ */
+ time_t start;
+
+ /**
+ * 1 if at daytime at the the time `.start`,
+ * 0 if at nighttime at the the time `.start`
+ */
+ double day_level;
+
+ /**
+ * `.next->day_level - .day_level` dividied by
+ * the duration of the period, in seconds
+ *
+ * `.day_level` is added to the number of seconds
+ * elapsed since `.start` multiplied by this number
+ * to get the dayness level at that time
+ */
+ double diff_over_duration;
+
+ /**
+ * The following time period
+ */
+ const struct time_period *next;
+};
+
+
+/**
+ * Dayness level scheme
+ */
+union scheme {
+ /**
+ * The scheme type
+ *
+ * If `STATIC_SCHEME`, the union contains no scheme data
+ */
+ enum scheme_type type;
+
+ /**
+ * Used if `.type == SOLAR_SCHEME`
+ */
+ struct solar_scheme {
+ /**
+ * `SOLAR_SCHEME`
+ */
+ enum scheme_type type;
+
+ /**
+ * The lowest solar elevation of daytime
+ */
+ double high;
+
+ /**
+ * The highest solar elevation of nighttime
+ */
+ double low;
+
+ /**
+ * `.high - .low`
+ */
+ double range;
+ } elevation;
+
+ /**
+ * Used if `.type == CLOCK_SCHEME`
+ */
+ struct clock_scheme {
+ /**
+ * `CLOCK_SCHEME`
+ */
+ enum scheme_type type;
+
+ /**
+ * Circularly linked list of time periods
+ *
+ * The application will update this to always
+ * point to the current time period
+ */
+ const struct time_period *periods;
+
+ /**
+ * The memory allocation for the nodes in `.periods`
+ */
+ struct time_period periods_array[4];
+ } time;
+};
+
+
+struct config_ini_setting {
+ struct config_ini_setting *next;
+ char *name;
+ char *value;
+};
+
+
+struct config_ini_section {
+ struct config_ini_section *next;
+ char *name;
+ struct config_ini_setting *settings;
+};
+
+
+struct config_ini_state {
+ struct config_ini_section *sections;
+};
+
+
+/**
+ * `int` valued setting (used for booleans) with setting source
+ */
+struct setting_i {
+ enum setting_source source; /**< Setting source */
+ int value; /**< Setting value */
+};
+
+/**
+ * `unsigned long int` valued setting with setting source
+ */
+struct setting_lu {
+ enum setting_source source; /**< Setting source */
+ unsigned long int value; /**< Setting value */
+};
+
+/**
+ * `double` valued setting with setting source
+ */
+struct setting_f {
+ enum setting_source source; /**< Setting source */
+ double value; /**< Setting value */
+};
+
+/**
+ * `double[3]` valued setting with setting source
+ */
+struct setting_f3 {
+ enum setting_source source; /**< Setting source */
+ double value[3]; /**< Setting values */
+};
+
+/**
+ * `time_t` valued setting with setting source
+ */
+struct setting_time {
+ enum setting_source source; /**< Setting source */
+ time_t value; /**< Setting value */
+};
+
+/**
+ * `char *` valued setting with setting source
+ */
+struct setting_str {
+ enum setting_source source; /**< Setting source */
+ char *value; /**< Setting value */
+};
+
+/**
+ * Intermediate settings representation of colour settings
+ * (for a non-transitional period) with settings sources
+ * used for determining whether settings from the configuration
+ * file (which is parsed after the command line) applied or are
+ * specified multiple times in the configuration file
+ */
+struct setting_colour {
+ /**
+ * Colour temperature, in Kelvin
+ *
+ * Set by the "-t" and "-o" options and the "temperature"
+ * ("temp") settings, optionally suffixed "-day" if
+ * a daytime setting or "-night" if a nighttime setting
+ */
+ struct setting_lu temperature;
+
+ /**
+ * Whitepoint brightness level, as a [0, 1] value
+ *
+ * Set by the "-b" option and the "brightness" settings,
+ * optionally suffixed "-day" if a daytime setting or
+ * "-night" if a nighttime setting
+ */
+ struct setting_f brightness;
+
+ /**
+ * Gamma values, in the order red, green, blue
+ *
+ * Set by the "-g" option and the "gamma" settings,
+ * optionally suffixed "-day" if a daytime setting or
+ * "-night" if a nighttime setting
+ */
+ struct setting_f3 gamma;
+};
+
+/**
+ * Intermediate settings representation with settings sources
+ * used for determining whether settings from the configuration
+ * file (which is parsed after the command line) applied or
+ * are specified multiple times in the configuration file
+ *
+ * Settings without a source can only be specified in the
+ * command line
+ */
+struct settings {
+ /**
+ * The path to the configuration, `NULL` if unspecified
+ * (search default paths)
+ *
+ * This represents the "-c" option
+ */
+ const char *config_file;
+
+ /**
+ * Scheme type to use as according the the command line
+ * options, `UNSPECIFIED_SCHEME` if not unspecified
+ */
+ enum scheme_type scheme_type;
+
+ /**
+ * Whether the program should, if ran in one-shot mode,
+ * pause after applying the effect and reset them when
+ * killed
+ *
+ * This represents the "-d" option
+ */
+ int until_death;
+
+ /**
+ * The path to the hook file or hook directory, `NULL`
+ * if unspecified (search default paths)
+ *
+ * This represents the "hook" setting and "-H" option
+ */
+ struct setting_str hook_file;
+
+ /**
+ * Whether the program should preserve preapplied
+ * colour calibrations
+ *
+ * This represents the "preserve-gamma" setting and
+ * "-P" (off) and "+P" (on) options
+ */
+ struct setting_i preserve_gamma;
+
+ /**
+ * Whether smooth transitions shall be applied when
+ * a large change in colour settings occurs
+ *
+ * This represents the "fade" setting (or the deprecated
+ * alias "transition") and "-r" (off) and "+r" (on) options
+ */
+ struct setting_i use_fade;
+
+ /**
+ * Whether the program should start in disabled mode
+ *
+ * This represents the "start-disabled" setting and
+ * "-D" (off) and "+D" (on) options
+ */
+ struct setting_i disabled;
+
+ /**
+ * The colour settings to apply at daytime
+ */
+ struct setting_colour day;
+
+ /**
+ * The colour settings to apply at nighttime
+ */
+ struct setting_colour night;
+
+ /**
+ * The lowest solar elevation, in degrees, at daytime,
+ * when using solar scheme
+ *
+ * This represents the "elevation-high" setting and the
+ * left value of the -e option
+ */
+ struct setting_f elevation_high;
+
+ /**
+ * The highest solar elevation, in degrees, at nighttime,
+ * when using solar scheme
+ *
+ * This represents the "elevation-low" setting
+ *
+ * This represents the "elevation-low" setting and the
+ * right value of the -e option
+ */
+ struct setting_f elevation_low;
+
+ /**
+ * The wall-clock time that marks the end of nighttime,
+ * when using clock scheme
+ *
+ * This represents the left value of the "dawn-time" setting
+ */
+ struct setting_time dawn_start; /* TODO no cmdline option */
+
+ /**
+ * The wall-clock time that marks the start of daytime,
+ * when using clock scheme
+ *
+ * This represents the right value of the "dawn-time" setting
+ */
+ struct setting_time dawn_end; /* TODO no cmdline option */
+
+ /**
+ * The wall-clock time that marks the end of daytime,
+ * when using clock scheme
+ *
+ * This represents the left value of the "dusk-time" setting
+ */
+ struct setting_time dusk_start; /* TODO no cmdline option */
+
+ /**
+ * The wall-clock time that marks the start of nighttime,
+ * when using clock scheme
+ *
+ * This represents the right value of the "dusk-time" setting
+ */
+ struct setting_time dusk_end; /* TODO no cmdline option */
+
+ /* Selected gamma method */
+ const struct gamma_method *method;
+ /* Arguments for gamma method */
+ char *method_args;
+
+ /* Selected location provider */
+ const struct location_provider *provider;
+ /* Arguments for location provider */
+ char *provider_args;
+
+ struct config_ini_state config;
+};
+
+
+/**
+ * Adjustment method information and interface
+ */
+struct gamma_method {
+ /**
+ * The name of the adjustment method
+ */
+ const char *name;
+
+ /**
+ * 1 if the method should be tried if none is explicitly chosen,
+ * 0 otherwise
+ */
+ int autostart;
+
+ /**
+ * 1 if the method automatically resets the adjustments when disconnected,
+ * 0 otherwise
+ */
+ int autoreset;
+
+ /**
+ * Check if the adjustment method is available in the used backend
+ *
+ * @return 1 if the adjustment method is available, 0 otherwise
+ */
+ int (*is_available)(void);
+
+ /**
+ * Create an initialised state object
+ *
+ * @param state_out Output parameter for the state object
+ * @return 0 on success, -1 on failure
+ *
+ * `*state_out` is set (potentially to `NULL`) on failure
+ */
+ int (*create)(GAMMA_STATE **state_out);
+
+ /**
+ * Configure the adjustment method
+ *
+ * @param state State object for the adjustment method
+ * @param key Option to configure
+ * @param value Option value to set
+ * @return 0 on success, -1 on failure
+ */
+ int (*set_option)(GAMMA_STATE *state, const char *key, const char *value);
+
+ /**
+ * Print help on options for the adjustment method
+ */
+ void (*print_help)(void);
+
+ /**
+ * Finalise option-dependent initialisation and connections
+ *
+ * @param state State object for the adjustment method
+ * @return 0 on success, -1 on failure
+ */
+ int (*start)(GAMMA_STATE *state);
+
+ /**
+ * Apply colour settings
+ *
+ * @param state State object for the adjustment method
+ * @param settings The colour settings to apply
+ * @param preserve Whether currently applied adjustments (assumed
+ * to be colour calibration) shall remain applied
+ * @return 0 on success, -1 on failure
+ */
+ int (*apply)(GAMMA_STATE *state, const struct colour_setting *setting, int preserve);
+
+ /**
+ * Restore the adjustments to the `.state` before start was called
+ *
+ * @param state State object for the adjustment method
+ */
+ void (*restore)(GAMMA_STATE *state);
+
+ /**
+ * Close connections and deallocate all state resources
+ *
+ * @param state The state to terminate
+ *
+ * The pointer `state` will become invalid
+ */
+ void (*free)(GAMMA_STATE *state);
+};
+
+/**
+ * Initialiser for `struct gamma_method`
+ *
+ * @param NAME:const char * Value for `.name`
+ * @param AUTOSTART:int Value for `.autostart`
+ * @param AUTORESET:int Value for `.autoreset`
+ * @param PREFIX:identifier The text, sans terminal underscore (_), prefixed to the
+ * names of each function implementing the adjustment method
+ */
+#define GAMMA_METHOD_INIT(NAME, AUTOSTART, AUTORESET, PREFIX)\
+ {\
+ .name = (NAME),\
+ .autostart = (AUTOSTART),\
+ .autoreset = (AUTORESET),\
+ .is_available = &PREFIX##_is_available,\
+ .create = &PREFIX##_create,\
+ .set_option = &PREFIX##_set_option,\
+ .print_help = &PREFIX##_print_help,\
+ .start = &PREFIX##_start,\
+ .free = &PREFIX##_free,\
+ .restore = &PREFIX##_restore,\
+ .apply = &PREFIX##_apply\
+ }
+
+
+/**
+ * Location provider information and interface
+ */
+struct location_provider {
+ /**
+ * The name of the location provider
+ */
+ const char *name;
+
+ /**
+ * Create an initialised state object
+ *
+ * @param state_out Output parameter for the state object
+ * @return 0 on success, -1 on failure
+ *
+ * `*state_out` is set (potentially to `NULL`) on failure
+ */
+ int (*create)(LOCATION_STATE **state_out);
+
+ /**
+ * Configure the location provider
+ *
+ * @param state State object for the location provider
+ * @param key Option to configure
+ * @param value Option value to set
+ * @return 0 on success, -1 on failure
+ */
+ int (*set_option)(LOCATION_STATE *state, const char *key, const char *value);
+
+ /**
+ * Print help on options for the location provider
+ */
+ void (*print_help)(void);
+
+ /**
+ * Finalise option-dependent initialisation and connections
+ *
+ * @param state State object for the location provider
+ * @return 0 on success, -1 on failure
+ */
+ int (*start)(LOCATION_STATE *state);
+
+ /**
+ * Get the file descriptor used by the location provider
+ *
+ * The application may use it for detecting when there
+ * is data available for `.handle` to act upon
+ *
+ * @param state State object for the location provider
+ * @return The file descriptor used by location provider, -1 if none
+ */
+ int (*get_fd)(LOCATION_STATE *state);
+
+ /**
+ * Get the current location
+ *
+ * This function shall only be caused if `.get_fd` returns -1
+ * or the file descriptor it returns has data available on it
+ * as indicated by input polling, otherwise `*location` and
+ * `*available` will not be set
+ *
+ * @param state State object for the location provider
+ * @param location_out Output parameter for the current location
+ * @param available_out Output parameter for whether the location provider
+ * is currently available
+ * @return 0 on success, -1 on unrecoverable failure
+ */
+ int (*fetch)(LOCATION_STATE *state, struct location *location, int *available);
+
+ /**
+ * Close connections and deallocate all state resources
+ *
+ * @param state The state to terminate
+ *
+ * The pointer `state` will become invalid
+ */
+ void (*free)(LOCATION_STATE *state);
+};
+
+/**
+ * Initialiser for `struct location_provider`
+ *
+ * @param NAME:const char * Value for `.name`
+ * @param PREFIX:identifier The text, sans terminal underscore (_), prefixed to the
+ * names of each function implementing the location provider
+ */
+#define LOCATION_PROVIDER_INIT(NAME, PREFIX)\
+ {\
+ .name = (NAME),\
+ .create = &PREFIX##_create,\
+ .set_option = &PREFIX##_set_option,\
+ .print_help = &PREFIX##_print_help,\
+ .start = &PREFIX##_start,\
+ .get_fd = &PREFIX##_get_fd,\
+ .fetch = &PREFIX##_fetch,\
+ .free = &PREFIX##_free\
+ }
+
+
+/**
+ * `NULL` terminated list of adjustment methods
+ */
+extern const struct gamma_method *gamma_methods[];
+
+/**
+ * `NULL` terminated list of location providers
+ */
+extern const struct location_provider *location_providers[];
+
+/**
+ * Set to 1 once the process has received a signal to terminate
+ */
+extern volatile sig_atomic_t exiting;
+
+/**
+ * Set to 1 once the process has received a signal to remove its effect
+ */
+extern volatile sig_atomic_t disable;
+
+/**
+ * Bitwise or OR of received SIGUSR2 signal values
+ */
+extern volatile enum signals signals;
+
+/**
+ * The colour settings applied at daytime
+ */
+extern struct colour_setting day_settings;
+
+/**
+ * The colour settings applied at nighttime
+ */
+extern struct colour_setting night_settings;
+
+/**
+ * The colour settings applied at nighttime
+ */
+extern union scheme scheme;
+
+/**
+ * The mode the application is running in
+ */
+extern enum program_mode mode;
+
+/**
+ * Whether initially applied adjustments (assumed
+ * to be colour calibration) shall remain applied
+ */
+extern int preserve_gamma;
+
+/**
+ * Whether smooth transitions shall be applied when
+ * a large change in colour settings occurs
+ */
+extern int use_fade;
+
+/**
+ * Whether the application is in verbose mode
+ */
+extern int verbose;
+
+/**
+ * The path to the hook file or hook directory, `NULL`
+ * if unspecified (search default paths)
+ */
+extern char *hook_file;
+
+
+/* backend-direct.c */
+
+/**
+ * Create an initialised state object for direct gamma adjustments
+ *
+ * @param state_out Output parameter for the state object
+ * @param method libgamma constant for the adjustment method
+ * @param method_name redshift's name for the adjustment method
+ * @return 0 on success, -1 on failure
+ *
+ * `*state_out` is set (potentially to `NULL`) on failure
+ */
+int direct_create(GAMMA_STATE **state_out, int method, const char *method_name);
+
+/**
+ * Print help on options for the adjustment method using direct gamma adjustments
+ *
+ * @param method libgamma constant for the adjustment method
+ */
+void direct_print_help(int method);
+
+/**
+ * Configure the adjustment method using direct gamma adjustments
+ *
+ * @param state State object for the adjustment method
+ * @param key Option to configure
+ * @param value Option value to set
+ * @return 0 on success, -1 on failure
+ */
+int direct_set_option(GAMMA_STATE *state, const char *key, const char *value);
+
+/**
+ * Select site to apply adjustments to using direct gamma adjustments
+ *
+ * @param state State object for the adjustment method
+ * @param key Option to configure
+ * @param value Option value to set
+ * @return 0 on success, -1 on failure
+ */
+int direct_set_site(GAMMA_STATE *state, const char *key, const char *value);
+
+/**
+ * Select partitions to apply adjustments to using direct gamma adjustments
+ *
+ * @param state State object for the adjustment method
+ * @param key Option to configure
+ * @param value Option value to set
+ * @return 0 on success, -1 on failure
+ */
+int direct_set_partitions(GAMMA_STATE *state, const char *key, const char *value);
+
+/**
+ * Select CRTCs to apply adjustments to using direct gamma adjustments
+ *
+ * @param state State object for the adjustment method
+ * @param key Option to configure
+ * @param value Option value to set
+ * @return 0 on success, -1 on failure
+ */
+int direct_set_crtcs(GAMMA_STATE *state, const char *key, const char *value);
+
+/**
+ * Select EDIDs of outputs to apply adjustments to using direct gamma adjustments
+ *
+ * @param state State object for the adjustment method
+ * @param key Option to configure
+ * @param value Option value to set
+ * @return 0 on success, -1 on failure
+ */
+int direct_set_edids(GAMMA_STATE *state, const char *key, const char *value);
+
+/**
+ * Finalise option-dependent initialisation and connections
+ * for direct gamma adjustments
+ *
+ * @param state State object for the adjustment method
+ * @return 0 on success, -1 on failure
+ */
+int direct_start(GAMMA_STATE *state);
+
+/**
+ * Apply colour settings using direct gamma adjustments
+ *
+ * @param state State object for the adjustment method
+ * @param settings The colour settings to apply
+ * @param preserve Whether currently applied adjustments (assumed
+ * to be colour calibration) shall remain applied
+ * @return 0 on success, -1 on failure
+ */
+int direct_apply(GAMMA_STATE *state, const struct colour_setting *setting, int preserve);
+
+/**
+ * Restore the adjustments to the `.state` before start was called
+ * using direct gamma adjustments
+ *
+ * @param state State object for the adjustment method
+ */
+void direct_restore(GAMMA_STATE *state);
+
+/**
+ * Close connections and deallocate all state resources
+ * for direct gamma adjustments
+ *
+ * @param state The state to terminate
+ *
+ * The pointer `state` will become invalid
+ */
+void direct_free(GAMMA_STATE *state);
+
+
+/* colour.c */
+
+/**
+ * Interpolate between two colour settings
+ *
+ * @param a The first colour setting, used wholly when `t` is 0
+ * @param b The second colour setting, used wholly when `t` is 1
+ * @param t The degree to which `second` second be applied
+ * @param result Output parameter for `(1 - t) * a + t * b`
+ */
+void interpolate_colour_settings(const struct colour_setting *a, const struct colour_setting *b,
+ double t, struct colour_setting *result);
+
+/**
+ * Check whether the differences between two colours settings
+ * are large enough to warrant fading between the two
+ *
+ * @param a The first colour setting
+ * @param b The second colour setting
+ * @return 1 if the difference between `a` and `b` is large, 0 otherwise
+ */
+GCC_ONLY(__attribute__((__pure__)))
+int colour_setting_diff_is_major(const struct colour_setting *a, const struct colour_setting *b);
+
+#define LIST_RAMPS_STOP_VALUE_TYPES(X, D)\
+ X(u8, ramps8, uint8_t, UINT8_MAX, 8) D \
+ X(u16, ramps16, uint16_t, UINT16_MAX, 16) D\
+ X(u32, ramps32, uint32_t, UINT32_MAX, 32) D\
+ X(u64, ramps64, uint64_t, UINT64_MAX, 64) D\
+ X(float, rampsf, float, 1, -1) D\
+ X(double, rampsd, double, 1, -2)
+
+#define X(SUFFIX, RAMPS, TYPE, MAX, DEPTH)\
+ /**
+ * Fill the gamma ramps
+ *
+ * @param gamma_r The gamma ramp for the red channel
+ * @param gamma_g The gamma ramp for the green channel
+ * @param gamma_b The gamma ramp for the blue channel
+ * @param saved_r Saved gamma ramp with calibrations for
+ * the red channel to preserve, or `NULL`
+ * @param saved_g Saved gamma ramp with calibrations for
+ * the green channel to preserve, or `NULL`
+ * @param saved_b Saved gamma ramp with calibrations for
+ * the blue channel to preserve, or `NULL`
+ * @param size_r The number of stops in `gamma_r`
+ * @param size_g The number of stops in `gamma_g`
+ * @param size_b The number of stops in `gamma_b`
+ * @param settings The colour settings to apply (temperature, brightness, gamma)
+ */\
+ void fill_ramps_##SUFFIX(TYPE *gamma_r, TYPE *gamma_g, TYPE *gamma_b,\
+ const TYPE *saved_r, const TYPE *saved_g, const TYPE *saved_b,\
+ size_t size_r, size_t size_g, size_t size_b,\
+ const struct colour_setting *setting)
+LIST_RAMPS_STOP_VALUE_TYPES(X, ;);
+#undef X
+
+
+/* config-ini.c */
+
+/**
+ * Load the configuration file
+ *
+ * @param state Output parameter for the configurations
+ * @param path The path to the configuration file, or `NULL` if the
+ * application should look for it in the default paths
+ * @param pathbuf_out Output parameter for the memory allocated for the
+ * return value
+ * @return The path to the loaded configuration file, `NULL` if none
+ */
+const char *config_ini_init(struct config_ini_state *state, const char *path, char **pathbuf_out);
+
+/**
+ * Deallocate the settings loaded
+ * from the configurations file
+ *
+ * @param state The configurations
+ */
+void config_ini_free(struct config_ini_state *state);
+
+/**
+ * Get a section from the configuration file
+ *
+ * @param state The configurations
+ * @param name The name of the section
+ * @return The section; `NULL` if missing
+ */
+GCC_ONLY(__attribute__((__pure__)))
+struct config_ini_section *config_ini_get_section(struct config_ini_state *state, const char *name);
+
+
+/* config.c */
+
+/**
+ * Load settings
+ *
+ * @param settings Output parameter for the settings
+ * @param argc Number of command line arguments
+ * @param argv `NULL` terminated list of command line arguments,
+ * including argument zero
+ */
+void load_settings(struct settings *settings, int argc, char *argv[]);
+
+
+/* gamma.c */
+
+/**
+ * Get and configure adjustment method
+ *
+ * @param settings The loaded application settings, will be updated
+ * to point `settings->method` to the adjustment method
+ * @param method_state_out Output parameter for the state of the adjustment method
+ *
+ * The function will print an error message and exit the
+ * process if no adjustment method is available
+ */
+void acquire_adjustment_method(struct settings *settings, GAMMA_STATE **method_state_out);
+
+
+/* location.c */
+
+/**
+ * Get the current location from the location provider
+ *
+ * @param provider The location provider functions
+ * @param state The location provider state
+ * @param timeout The number of milliseconds to wait, -1 for indefinitely
+ * @param location_out Output parameter for the location, in GPS coordinates
+ * @return 1 if `*location_out` was updated,
+ * 0 if the timeout was reached,
+ * -1 on error
+ */
+int get_location(const struct location_provider *provider, LOCATION_STATE *state, int timeout, struct location *location_out);
+
+/**
+ * Get and configure location provider
+ *
+ * @param settings The loaded application settings, will be updated
+ * to point `settings->provider` to the location provider
+ * @param location_state_out Output parameter for the state of the location provider
+ *
+ * The function will print an error message and exit the
+ * process if no location provider is available
+ */
+void acquire_location_provider(struct settings *settings, LOCATION_STATE **location_state_out);
+
+/**
+ * Check whether location is valid
+ *
+ * If the message is invalid, and error message is printed
+ *
+ * @param location The location to check
+ * @return 1 if the location is valid, 0 otherwise
+ */
+int location_is_valid(const struct location *location);
+
+/**
+ * Print the current location to standard output
+ *
+ * @param location The current location
+ */
+void print_location(const struct location *location);
+
+
+/* hooks.c */
+
+/**
+ * Run hooks with a signal that the period changed
+ *
+ * @param prev_period The previous period
+ * @param period The new current period
+ */
+void run_period_change_hooks(enum period prev_period, enum period period);
+
+
+/* signals.c */
+
+/**
+ * Install signal handlers for the process
+ */
+void install_signal_handlers(void);
+
+/**
+ * Install signal handlers for forcefully terminating
+ * the process
+ *
+ * This is useful if slave thread is blocked
+ *
+ * SIGINT, SIGTERM, and SIGQUIT are set to at the
+ * first arrival arm a SIGARLM timer with a short
+ * expiration time, and on the second arrival
+ * immediately terminate the process. SIGARLM is
+ * set to immediately terminate the process.
+ */
+#ifndef WINDOWS
+void install_forceful_exit_signal_handlers(void);
+#endif
+
+
+/* util.c */
+
+/**
+ * Remove trailing whitespace
+ *
+ * @param s The string to trim, will be truncated
+ * @param end The current end of `s`; will be looked up if `NULL`
+ * @return `s`
+ */
+char *rtrim(char *s, char *end);
+
+/**
+ * Remove leading whitespace
+ *
+ * @param s The string to trim (will not be modified)
+ * @return `s` with an offset
+ */
+GCC_ONLY(__attribute__((__warn_unused_result__, __pure__)))
+char *ltrim(char *s);
+
+/**
+ * Get the user's home directory
+ *
+ * This function looks up the user's home directory
+ * once and caches the result, later calls to this
+ * function will use the cached result
+ *
+ * @return The user's home directory; the empty string if not found
+ */
+const char *get_home(void);
+
+/**
+ * Search for a file and open it for reading
+ *
+ * @param path_spec Specification for the path to try
+ * @param path_out Output parameter for the found file
+ * @param pathbuf_out Output parameter for the memory allocation for `*path_out`;
+ * shall be free(3)d by the caller
+ * @return `FILE` object for the reading the file; `NULL` if not found
+ */
+FILE *try_path_fopen(const struct env_path *path_spec, const char **path_out, char **pathbuf_out);
+
+/**
+ * Search for a directory and open it for reading
+ *
+ * @param path_spec Specification for the path to try
+ * @param path_out Output parameter for the found directory
+ * @param pathbuf_out Output parameter for the memory allocation for `*path_out`;
+ * shall be free(3)d by the caller
+ * @return `DIR` object for the reading the directory; `NULL` if not found
+ */
+DIR *try_path_opendir(const struct env_path *path_spec, const char **path_out, char **pathbuf_out);
+
+#ifndef WINDOWS
+/**
+ * Create a pipe(7) where both ends have `O_CLOEXEC`,
+ * the read-end will also have `O_NONBLOCK` applied
+ *
+ * @param pipefds Output parameter for the pipe's file descriptors:
+ * 0) reading file descriptor, and
+ * 1) writing file descriptor
+ */
+void pipe_rdnonblock(int pipefds[2]);
+#endif
+
+/**
+ * Wrapper for calloc(3) that prints and error message
+ * and terminates the process on failure
+ *
+ * @param n Number of elements to allocate memory for
+ * @param m The size, in bytes, of each element
+ * @return Pointer to zero-initialised storage of at least `n*m` bytes, with default alignment
+ */
+GCC_ONLY(__attribute__((__warn_unused_result__, __malloc__, __alloc_size__(1, 2), __returns_nonnull__)))
+void *ecalloc(size_t n, size_t m);
+
+/**
+ * Wrapper for malloc(3) that prints and error message
+ * and terminates the process on failure
+ *
+ * @param n Number of bytes to allocate
+ * @return Pointer to uninitialised storage of at least `n` bytes, with default alignment
+ */
+GCC_ONLY(__attribute__((__warn_unused_result__, __malloc__, __alloc_size__(1), __returns_nonnull__)))
+void *emalloc(size_t n);
+
+/**
+ * Wrapper for realloc(3) that prints and error message
+ * and terminates the process on failure
+ *
+ * @param ptr Pointer to reallocate
+ * @param n Despired allocation size in bytes
+ * @return Replacement pointer for `ptr`, pointing to storage of at least `n` bytes,
+ * any byte that could be copied from `ptr` is copied over, and additional
+ * memory is uninitialised
+ */
+GCC_ONLY(__attribute__((__warn_unused_result__, __returns_nonnull__, __alloc_size__(2))))
+void *erealloc(void *ptr, size_t n);
+
+/**
+ * Wrapper for strdup(3) that prints and error message
+ * and terminates the process on failure
+ *
+ * @param s String to copy
+ * @return Copy of `s`
+ */
+GCC_ONLY(__attribute__((__warn_unused_result__, __returns_nonnull__, __malloc__, __assume_aligned__(1), __nonnull__)))
+char *estrdup(const char *s);
+
+/**
+ * Print a message, prefixed with the process name (followed by ": "),
+ * to standard error
+ *
+ * @param fmt Message text format string, see fprintf(3)
+ * @param args Message text arguments
+ */
+GCC_ONLY(__attribute__((__nonnull__(1), __format__(__printf__, 1, 0))))
+void vweprintf(const char *fmt, va_list args);
+
+/**
+ * Print a message, prefixed with the process name (followed by ": "),
+ * to standard error
+ *
+ * @param fmt Message text format string, see fprintf(3)
+ * @param ... Message text arguments
+ */
+GCC_ONLY(__attribute__((__nonnull__(1), __format__(__printf__, 1, 2))))
+void weprintf(const char *fmt, ...);
+
+/**
+ * Print a message, prefixed with the process name (followed by ": "),
+ * to standard error and terminate the process with exit value
+ * indicating error
+ *
+ * @param fmt Message text format string, see fprintf(3)
+ * @param ... Message text arguments
+ */
+GCC_ONLY(__attribute__((__nonnull__(1), __format__(__printf__, 1, 2), __noreturn__)))
+void eprintf(const char *fmt, ...);
+
+
+extern const struct gamma_method dummy_gamma_method;
+#ifdef ENABLE_COOPGAMMA
+extern const struct gamma_method coopgamma_gamma_method;
+#endif
+extern const struct gamma_method randr_gamma_method;
+extern const struct gamma_method vidmode_gamma_method;
+extern const struct gamma_method drm_gamma_method;
+extern const struct gamma_method quartz_gamma_method;
+extern const struct gamma_method wingdi_gamma_method;
+
+extern const struct location_provider manual_location_provider;
+#ifdef ENABLE_GEOCLUE2
+extern const struct location_provider geoclue2_location_provider;
+#endif
+#ifdef ENABLE_CORELOCATION
+extern const struct location_provider corelocation_location_provider;
+#endif
+#ifndef WINDOWS
+extern const struct location_provider geofile_location_provider;
+extern const struct location_provider timezone_location_provider;
+#endif
+
+
+#if defined(__GNUC__)
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wfloat-equal"
+#endif
+static inline int
+exact_eq(double a, double b)
+{
+ return a == b;
+}
+#if defined(__GNUC__)
+# pragma GCC diagnostic pop
+#endif
diff --git a/redshift/config-ini.c b/redshift/config-ini.c
new file mode 100644
index 0000000..13aaa57
--- /dev/null
+++ b/redshift/config-ini.c
@@ -0,0 +1,267 @@
+/*-
+ * 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"
+
+
+/**
+ * Paths, in order of priority, to test when looking for
+ * the configuration file for redshift
+ */
+static const struct env_path paths[] = {
+ {0, "XDG_CONFIG_HOME", "/redshift-ng/redshift.conf"},
+ {0, "XDG_CONFIG_HOME", "/redshift/redshift.conf"},
+ {0, "XDG_CONFIG_HOME", "/redshift.conf"},
+#if defined(WINDOWS)
+ {0, "localappdata", "/redshift-ng/redshift.conf"},
+ {0, "localappdata", "/redshift/redshift.conf"},
+ {0, "localappdata", "/redshift.conf"},
+#endif
+ {0, "HOME", "/.config/redshift-ng/redshift.conf"},
+ {0, "HOME", "/.config/redshift/redshift.conf"},
+ {0, "HOME", "/.config/redshift.conf"},
+ {0, "HOME", "/.redshift.conf"},
+ {0, NULL, "/.config/redshift-ng/redshift.conf"},
+ {0, NULL, "/.config/redshift/redshift.conf"},
+ {0, NULL, "/.config/redshift.conf"},
+ {0, NULL, "/.redshift.conf"},
+ {1, "XDG_CONFIG_DIRS", "/redshift-ng/redshift.conf"},
+ {1, "XDG_CONFIG_DIRS", "/redshift/redshift.conf"},
+ {1, "XDG_CONFIG_DIRS", "/redshift.conf"},
+#if !defined(WINDOWS)
+ {0, "", "/etc/redshift-ng/redshift.conf"},
+ {0, "", "/etc/redshift/redshift.conf"},
+ {0, "", "/etc/redshift.conf"}
+#endif
+};
+
+
+/**
+ * Open the configuration file for reading
+ *
+ * @param path The path to the configuration file, or `NULL` if the
+ * application should look for it in the default paths
+ * @param path_out Output parameter for the configuration file path
+ * @param pathbuf_out Output parameter for the memory allocation for `*path_out`;
+ * will be set to `NULL` unless `path` is `NULL`; shall be
+ * free(3)d by the caller
+ * @param should_close_out Output parameter for whether the file should be closed
+ * @return `FILE` object for the reading the file; `NULL` if not
+ * found and `path` is `NULL`
+ */
+static FILE *
+open_config_file(const char *path, const char **path_out, char **pathbuf_out, int *should_close_out)
+{
+ FILE *f = NULL;
+ size_t i;
+#ifndef WINDOWS
+ const char *s;
+ int fd, old_fd = -1;
+#endif
+
+ *path_out = path;
+ *pathbuf_out = NULL;
+ *should_close_out = 1;
+
+ if (!path) {
+ for (i = 0; !f && i < ELEMSOF(paths); i++)
+ f = try_path_fopen(&paths[i], path_out, pathbuf_out);
+ if (f)
+ weprintf(_("Found configuration file `%s'."), *path_out);
+ else
+ weprintf(_("No configuration file found."));
+ } else if (!strcmp(path, "/dev/null")) { /* needed to allow /dev/null to be specified on Windows */
+ return NULL;
+ } else if (!strcmp(path, "-")) {
+ *should_close_out = 0;
+ return stdin;
+#ifndef WINDOWS
+ } else if (!strcmp(path, "/dev/stdin")) {
+ *should_close_out = 0;
+ return stdin;
+ } else if (!strcmp(path, "/dev/stdout")) {
+ fd = STDOUT_FILENO;
+ goto use_fd;
+ } else if (!strcmp(path, "/dev/stderr")) {
+ fd = STDERR_FILENO;
+ goto use_fd;
+ } else if (!strncmp(path, "/dev/fd/", sizeof("/dev/fd/") - 1U)) {
+ s = &path[sizeof("/dev/fd/") - 1U];
+# if defined(__linux__)
+ goto parse_fd;
+ } else if (!strncmp(path, "/proc/self/fd/", sizeof("/proc/self/fd/") - 1U)) {
+ s = &path[sizeof("/proc/self/fd/") - 1U];
+ parse_fd:
+# endif
+ fd = 0;
+ if (!*s)
+ goto fallback;
+ while (isdigit(*s)) {
+ if (fd > (INT_MAX - (*s & 15)) / 10)
+ goto fallback;
+ fd = fd * 10 + (*s & 15);
+ }
+ if (*s)
+ goto fallback;
+ use_fd:
+ if (fd > 2) {
+ fd = dup(old_fd = fd);
+ if (fd < 0)
+ eprintf("dup %i:", old_fd);
+ }
+ f = fdopen(fd, "r");
+ if (!f) {
+ if (old_fd < 0)
+ eprintf("fdopen %i \"r\":", fd);
+ else
+ eprintf("fdopen <duplicate of %i> \"r\":", old_fd);
+ }
+#endif
+ } else {
+#ifndef WINDOWS
+ fallback:
+#endif
+ f = fopen(path, "r");
+ if (!f)
+ eprintf("fopen %s \"r\":", path);
+ }
+
+ return f;
+}
+
+
+const char *
+config_ini_init(struct config_ini_state *state, const char *path, char **pathbuf_out)
+{
+ struct config_ini_section *section = NULL;
+ struct config_ini_setting *setting;
+ char *line = NULL, *s, *p, *value, *end;
+ char *next_line = NULL, *name;
+ size_t size = 0;
+ ssize_t len = 0; /* initialised to silence false warning from clang */
+ FILE *f;
+ int should_close;
+
+ state->sections = NULL;
+ *pathbuf_out = NULL;
+
+ f = open_config_file(path, &path, pathbuf_out, &should_close);
+ if (!f)
+ return NULL;
+
+#ifndef WINDOWS
+again:
+#endif
+ while ((s = next_line) || (len = getline(&line, &size, f)) >= 0) {
+ if (!s && (s = line, strlen(s) != (size_t)len))
+ eprintf(_("Config file contains NUL byte."));
+
+ s = ltrim(s);
+ next_line = NULL;
+ end = &s[strcspn(s, "\r\n")];
+ if (*end) {
+ p = end;
+ do {
+ *p++ = '\0';
+ } while (*p == '\r' || *p == '\n');
+ if (*p)
+ next_line = p;
+ }
+ rtrim(s, end);
+
+ switch (*s) {
+ case ';': /* comment line */
+ case '#': /* comment line */
+ case '\0': /* blank line */
+ break;
+
+ case '[': /* "[%s]", section */
+ end = strchr(name = &s[1], ']');
+ if (!end || end[1] || end == name)
+ eprintf(_("Malformed section header in config file."));
+ *end = '\0';
+ section = emalloc(sizeof(*section));
+ section->name = estrdup(name);
+ section->settings = NULL;
+ section->next = state->sections;
+ state->sections = section;
+ break;
+
+ default: /* "%s = %s", name, value */
+ value = p = strchr(name = s, '=');
+ if (!value || value == name)
+ eprintf(_("Malformed assignment in config file."));
+ *value++ = '\0';
+ if (!section)
+ eprintf(_("Assignment outside section in config file."));
+ setting = emalloc(sizeof(*setting));
+ setting->name = estrdup(rtrim(name, p));
+ setting->value = estrdup(rtrim(ltrim(value), NULL));
+ setting->next = section->settings;
+ section->settings = setting;
+ break;
+ }
+ }
+ if (ferror(f)) {
+#ifndef WINDOWS
+ if (errno == EINTR) {
+ clearerr(f);
+ goto again;
+ }
+#endif
+ eprintf("getline %s:", path);
+ }
+
+ free(line);
+ if (should_close)
+ fclose(f);
+
+ return path;
+}
+
+
+void
+config_ini_free(struct config_ini_state *state)
+{
+ struct config_ini_section *section, *section_next;
+ struct config_ini_setting *setting, *setting_next;
+ for (section = state->sections; section; section = section_next) {
+ section_next = section->next;
+ for (setting = section->settings; setting; setting = setting_next) {
+ setting_next = setting->next;
+ free(setting->name);
+ free(setting->value);
+ free(setting);
+ }
+ free(section->name);
+ free(section);
+ }
+}
+
+
+struct config_ini_section *
+config_ini_get_section(struct config_ini_state *state, const char *name)
+{
+ /* TODO deal with multiple section definitions */
+ struct config_ini_section *section;
+ for (section = state->sections; section; section = section->next)
+ if (!strcasecmp(section->name, name))
+ return section;
+ return NULL;
+}
diff --git a/redshift/config.c b/redshift/config.c
new file mode 100644
index 0000000..29c5a53
--- /dev/null
+++ b/redshift/config.c
@@ -0,0 +1,1172 @@
+/*-
+ * 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"
+
+
+/**
+ * Output usage synopsis, without exiting
+ *
+ * @param f Output sink
+ */
+static void
+usage_no_exit(FILE *f)
+{
+ fprintf(f, _("Usage: %s %s\n"), argv0,
+ _("[-b brightness] [-c config-file] [-D | +D] [-E | +E | -e elevations] "
+ "[-g gamma] [-H hook-file] [-l latitude:longitude | -l provider[:options]] "
+ "[-m method[:options]] [-P | +P] [-r | +r] [-dv] "
+ "[-O temperature | -o | -p | -t temperature | -x] | -h | -V"));
+}
+
+
+/**
+ * Output usage synopsis and exit
+ */
+static void
+usage(void)
+{
+ usage_no_exit(stderr);
+ exit(1);
+}
+
+
+struct colour_setting day_settings;
+struct colour_setting night_settings;
+union scheme scheme = {.type = SOLAR_SCHEME};
+enum program_mode mode = PROGRAM_MODE_CONTINUAL;
+int preserve_gamma;
+int use_fade;
+int verbose = 0;
+char *hook_file;
+
+
+/**
+ * Print general help text
+ */
+static void
+print_help(void)
+{
+ usage_no_exit(stdout);
+ printf("\n");
+
+ printf(_("Automatically adjust display colour temperature according the Sun\n"));
+ printf("\n");
+
+ printf(_(" -b day:night Select whitepoint brightness (Default: 1:1)\n"));
+ printf(_(" -c file Load settings from specified configuration file\n"));
+ printf(_(" -D Start in enabled state (Default)\n"));
+ printf(_(" +D Start in disabled state\n"));
+ printf(_(" -d Keep the process alive and remove the colour effects when killed\n"));
+ printf(_(" -E Use wall-clock based schedule\n"));
+ printf(_(" +E Use solar elevation based schedule\n"));
+ printf(_(" -e day:night Select solar elevation thresholds for day and night (Default: %g:%g)\n"),
+ DEFAULT_HIGH_ELEVATION, DEFAULT_LOW_ELEVATION);
+ printf(_(" -g day:night Additional gamma correction (Default: 1:1:1:1:1:1)\n"));
+ printf(_(" -H hook-file Select hook file or directrory\n"));
+ printf(_(" -h Display this help message\n"));
+ printf(_(" -l lat:lon Specific geographical location\n"));
+ printf(_(" -l provider[:options] Select location provider to get geographical location\n"));
+ printf(_(" (Use `-l list' to list available providers)\n"));
+ printf(_(" -m method[:options] Select adjustment method for applying colour settings\n"));
+ printf(_(" (Use `-m list' to list adjustment methods)\n"));
+ printf(_(" -O day:night Select colour temperature and apply once\n"));
+ printf(_(" -o Apply colour settings once, then exit\n"));
+ printf(_(" -P Remove preexisting colour adjustments\n"));
+ printf(_(" +P Preserve preexisting colour adjustments (Default)\n"));
+ printf(_(" -p Print parameters and exit\n"));
+ printf(_(" -r Disable fading between colour adjustments\n"));
+ printf(_(" +r Enable fading between colour adjustments (Default)\n"));
+ printf(_(" -t day:night Select colour temperature and apply continually (Default: %lu:%lu\n"),
+ DEFAULT_DAY_TEMPERATURE, DEFAULT_NIGHT_TEMPERATURE);
+ printf(_(" -V Show program implementation and version\n"));
+ printf(_(" -v Enable verbose output\n"));
+ printf(_(" -x Remove adjustments from screen\n"));
+
+ printf("\n");
+ printf(_("This is a breif summary, see `%s' for more information.\n"), "man redshift");
+ printf("\n");
+ printf(_("The neutral temperature is %luK. Using this value will not change the color\n"
+ "temperature of the display. Setting the color temperature to a value higher\n"
+ "than this results in more blue light, and setting a lower value will result in\n"
+ "more red light.\n"), NEUTRAL_TEMPERATURE);
+ printf("\n");
+}
+
+
+/**
+ * Parse a boolean string
+ *
+ * @param s The string to parse
+ * @param key The name of the configuration assigned the value `s`
+ * @return 1 if `s` represents true, 0 if `s` represents false
+ */
+GCC_ONLY(__attribute__((__pure__)))
+static int
+get_boolean(const char *s, const char *key)
+{
+ int ret;
+ if (s[0] == '0' || s[0] == '1') {
+ ret = s[0] - '0';
+ if (s[1])
+ goto bad;
+ } else {
+ bad:
+ eprintf(_("Value of configuration `%s' must be either `1' or `0'."), key);
+ }
+ return ret;
+}
+
+
+/**
+ * atof(3)-like wrapper for strtod(3) that checks that the string is valid
+ *
+ * @param s The string to parse
+ * @param key The name of the configuration assigned the value `s`
+ * @return The encoded value
+ */
+static double
+checked_atof(const char *s, const char *key)
+{
+ double ret;
+ errno = 0;
+ ret = strtod(s, (void *)&s);
+ if (errno || *s)
+ eprintf(_("Value of configuration `%s' is not a value decimal value."), key);
+ return ret;
+}
+
+
+/**
+ * Split a list of values, and remove leading and trailing whitespace
+ * from each value
+ *
+ * @param s The string to split; will be modified
+ * @param count The number of despired strings
+ * @param strs Output array for the strings; each element will
+ * be set to `NULL` or `s` with an offset
+ * @param delim The character `s` shall be split by, cannot be
+ * a whitespace character
+ * @return 1 if `s` is valid, 0 otherwise
+ *
+ * If `count` is 1,
+ * the value most be specified,
+ * if `count` is 2,
+ * at least one of values must be specified
+ * if `count` is 3,
+ * all values most be specified, otherwise `s` must only contain
+ * on value, and no colon, which will be used from each output value, or
+ * if `count` is 6,
+ * `s` must be on one of the formats:
+ * `a`:
+ * the specified value is used for each output value,
+ * `a:b`:
+ * the three lower output values will be set to `a`, which may be empty/`NULL`, and
+ * the three upper output values will be set to `b`, which may be empty/`NULL`;
+ * at least `a` or `b` must be non-empty,
+ * `a:b:c`:
+ * each value most be specified, `s` will be interpreted as `a:b:c:a:b:c`,
+ * `:a:b:c`:
+ * each value most be specified, the lower three values be set to `NULL`,
+ * and `a`, `b`, `c` will be used fo the upper three values,
+ * `a:b:c:`:
+ * each value most be specified, the upper three values be set to `NULL`,
+ * and `a`, `b`, `c` will be used fo the lower three values, or
+ * `a:b:c:d:e:f`:
+ * each value most be specified;
+ * where ':' represents `delim`
+ *
+ * Summarily said, `s` may contain a scalar value or a 3-tuple, and it may
+ * also contain a value or one value for daytime and one value or nighttime.
+ * If configured to use 3-tuple but scalar is provided, the provided value is
+ * used for each of the 3 requested values. If configured to use daytime and
+ * nighttime, but only one is specified it is used for both, but if `s`
+ * starts with `delim`, daytime is skipped but if `s` ends with `delim`,
+ * nighttime, is skipped; but both cannot be skipped.
+ */
+static int
+get_strings(char *s, int count, char *strs[], char delim)
+{
+ int i = 0, n;
+
+ /* Split by colon and left-trim */
+ for (i = 0; i < count;) {
+ strs[i++] = s = ltrim(s);
+ s = strchr(s, delim);
+ if (!s)
+ break;
+ *s++ = '\0';
+ }
+ n = i;
+
+ /* Confirm no excess strings */
+ if (s && *ltrim(s))
+ return 0;
+
+ /* Right-trim and replace empty strings with NULL */
+ for (i = 0; i < n; i++)
+ if (!*rtrim(strs[i], NULL))
+ strs[i] = NULL;
+
+ /* Validate NULLs */
+ switch (n) {
+ case 1:
+ /* must be specified */
+ if (!strs[0])
+ return 0;
+ break;
+ case 2:
+ /* at least one most be specified */
+ if (!strs[0] && !strs[1])
+ return 0;
+ break;
+ case 3:
+ /* each most be specified */
+ if (!strs[0] || !strs[1] || !strs[2])
+ return 0;
+ break;
+ case 4:
+ /* exactly either the first or the last shall be NULL */
+ if (!strs[0] == !strs[3] || !strs[1] || !strs[2])
+ return 0;
+ break;
+ case 6:
+ /* each most be specified */
+ if (!strs[0] || !strs[1] || !strs[2] || !strs[3] || !strs[4] || !strs[5])
+ return 0;
+ break;
+ default:
+ /* n==5 is always invalid */
+ return 0;
+ }
+
+ /* Duplicate to fill `strs` */
+ switch (count) {
+ case 2:
+ if (n == 1)
+ strs[1] = strs[0];
+ break;
+ case 3:
+ if (n == 1)
+ strs[2] = strs[1] = strs[0];
+ else if (n == 2)
+ return 0;
+ break;
+ case 6:
+ if (n == 1) {
+ strs[5] = strs[4] = strs[3] = strs[2] = strs[1] = strs[0];
+ } else if (n == 2) {
+ strs[5] = strs[4] = strs[3] = strs[1];
+ strs[2] = strs[1] = strs[0];
+ } else if (n == 3) {
+ strs[5] = strs[2];
+ strs[4] = strs[1];
+ strs[3] = strs[0];
+ } else if (n == 4 && !strs[0]) {
+ strs[5] = strs[3];
+ strs[4] = strs[2];
+ strs[3] = strs[1];
+ strs[2] = NULL;
+ strs[1] = NULL;
+ } else if (n == 4) {
+ strs[5] = NULL;
+ strs[4] = NULL;
+ }
+ break;
+ default:
+ break;
+ }
+
+ return 1;
+}
+
+
+/**
+ * Parse and set temperature settings
+ *
+ * @param str The temperature specification to parse
+ * @param day The currently specified temperature for daytime,
+ * will be updated; `NULL` if it shall not be set
+ * @param night The currently specified temperature for nighttime,
+ * will be updated; `NULL` if it shall not be set
+ * @param key The configuration file setting being parsed,
+ * `NULL` if parsing the command line
+ */
+static void
+set_temperature(char *str, struct setting_lu *day, struct setting_lu *night, const char *key)
+{
+ struct setting_lu *settings[] = {day, night};
+ enum setting_source source = key ? SETTING_CONFIGFILE : SETTING_CMDLINE;
+ char *strs[2], *end;
+ size_t i, j;
+
+ if (!get_strings(str, !!day + !!night, strs, ':')) {
+ invalid:
+ weprintf(_("Malformed temperature argument."));
+ eprintf(_("Try `-h' for more information."));
+ }
+
+ errno = 0;
+ for (i = 0, j = 0; i < ELEMSOF(settings); j += settings[i++] ? 1 : 0) {
+ if (!settings[i] || !strs[j] || settings[i]->source > source)
+ continue;
+ if (settings[i]->source & SETTING_CONFIGFILE) {
+ if (i == 0)
+ weprintf(_("Daytime temperature specified multiple times in configuration file."));
+ else
+ weprintf(_("Night temperature specified multiple times in configuration file."));
+ }
+ settings[i]->source |= source;
+ settings[i]->value = strtoul(strs[j], &end, 10);
+ if (errno || end[*end == 'K'])
+ goto invalid;
+ if (!WITHIN(MIN_TEMPERATURE, settings[i]->value, MAX_TEMPERATURE))
+ eprintf(_("Temperature must be between %luK and %luK."), MIN_TEMPERATURE, MAX_TEMPERATURE);
+ }
+}
+
+
+/**
+ * Parse and set whitepoint brightness settings
+ *
+ * @param str The brightness specification to parse
+ * @param day The currently specified brightness for daytime,
+ * will be updated; `NULL` if it shall not be set
+ * @param night The currently specified brightness for nighttime,
+ * will be updated; `NULL` if it shall not be set
+ * @param key The configuration file setting being parsed,
+ * `NULL` if parsing the command line
+ */
+static void
+set_brightness(char *str, struct setting_f *day, struct setting_f *night, const char *key)
+{
+ struct setting_f *settings[] = {day, night};
+ enum setting_source source = key ? SETTING_CONFIGFILE : SETTING_CMDLINE;
+ char *strs[2], *end;
+ size_t i, j;
+
+ if (!get_strings(str, !!day + !!night, strs, ':')) {
+ invalid:
+ weprintf(_("Malformed brightness argument."));
+ eprintf(_("Try `-h' for more information."));
+ }
+
+ errno = 0;
+ for (i = 0, j = 0; i < ELEMSOF(settings); j += settings[i++] ? 1 : 0) {
+ if (!settings[i] || !strs[j] || settings[i]->source > source)
+ continue;
+ if (settings[i]->source & SETTING_CONFIGFILE) {
+ if (i == 0)
+ weprintf(_("Daytime brightness specified multiple times in configuration file."));
+ else
+ weprintf(_("Night brightness specified multiple times in configuration file."));
+ }
+ settings[i]->source |= source;
+ settings[i]->value = strtod(strs[j], &end);
+ if (errno || *end)
+ goto invalid;
+ if (!WITHIN(MIN_BRIGHTNESS, settings[i]->value, MAX_BRIGHTNESS))
+ eprintf(_("Brightness values must be between %.1f and %.1f."), MIN_BRIGHTNESS, MAX_BRIGHTNESS);
+ }
+}
+
+
+/**
+ * Parse and set gamma settings
+ *
+ * @param str The gamma specification to parse
+ * @param day The currently specified gamma for daytime,
+ * will be updated; `NULL` if it shall not be set
+ * @param night The currently specified gamma for nighttime,
+ * will be updated; `NULL` if it shall not be set
+ * @param key The configuration file setting being parsed,
+ * `NULL` if parsing the command line
+ */
+static void
+set_gamma(char *str, struct setting_f3 *day, struct setting_f3 *night, const char *key)
+{
+ struct setting_f3 *settings[] = {day, night};
+ enum setting_source source = key ? SETTING_CONFIGFILE : SETTING_CMDLINE;
+ char *strs[6], *end;
+ size_t i, j, k;
+
+ if (!get_strings(str, 3 * (!!day + !!night), strs, ':')) {
+ invalid:
+ weprintf(_("Malformed gamma argument."));
+ eprintf(_("Try `-h' for more information."));
+ }
+
+ errno = 0;
+ for (i = 0, j = 0; i < ELEMSOF(settings); j += settings[i++] ? 3 : 0) {
+ if (!settings[i] || !strs[j] || settings[i]->source > source)
+ continue;
+ if (settings[i]->source & SETTING_CONFIGFILE) {
+ if (i == 0)
+ weprintf(_("Daytime gamma specified multiple times in configuration file."));
+ else
+ weprintf(_("Night gamma specified multiple times in configuration file."));
+ }
+ settings[i]->source |= source;
+ for (k = 0; k < 3; k++) {
+ settings[i]->value[k] = strtod(strs[j + k], &end);
+ if (errno || *end)
+ goto invalid;
+ if (!WITHIN(MIN_GAMMA, settings[i]->value[k], MAX_GAMMA))
+ eprintf(_("Gamma values must be between %.1f and %.1f."), MIN_GAMMA, MAX_GAMMA);
+ }
+ }
+}
+
+
+/**
+ * Parse and set solar elevation settings
+ *
+ * The fucntion assumes that the setting source is the command line
+ *
+ * @param str The gamma specification to parse
+ * @param day The currently specified lowest solar elevation for
+ * daytime, will be updated; `NULL` if it shall not be set
+ * @param night The currently specified highest solar elevation for
+ * nighttime, will be updated; `NULL` if it shall not be set
+ */
+static void
+set_elevations(char *str, struct setting_f *day, struct setting_f *night)
+{
+ struct setting_f *settings[] = {day, night};
+ const enum setting_source source = SETTING_CMDLINE;
+ char *strs[2], *end;
+ size_t i, j;
+
+ if (!get_strings(str, !!day + !!night, strs, ':')) {
+ invalid:
+ weprintf(_("Malformed solar elevation argument."));
+ eprintf(_("Try `-h' for more information."));
+ }
+
+ errno = 0;
+ for (i = 0, j = 0; i < ELEMSOF(settings); j += settings[i++] ? 1 : 0) {
+ if (!settings[i] || !strs[j] || settings[i]->source > source)
+ continue;
+ settings[i]->source |= source;
+ settings[i]->value = strtod(strs[j], &end);
+ if (errno || *end)
+ goto invalid;
+ }
+}
+
+
+/**
+ * Parse a time string on either of the formats "HH:MM" and "HH:MM:SS"
+ *
+ * Times up to, but excluding, 48:00 are supported.
+ *
+ * Leap seconds are not supported
+ *
+ * @param str String to parse
+ * @return The represented time, -1 if malformed
+ */
+static time_t
+parse_time(char *str)
+{
+ time_t ret;
+ unsigned long int v;
+
+ errno = 0;
+
+ if (!isdigit(*str))
+ return -1;
+ v = strtoul(str, &str, 10);
+ if (errno || *str++ != ':' || v >= 48UL)
+ return -1;
+ ret = (time_t)(v * 60UL * 60UL);
+
+ if (!isdigit(*str))
+ return -1;
+ v = strtoul(str, &str, 10);
+ if (errno || v >= 60UL)
+ return -1;
+ ret += (time_t)(v * 60UL);
+
+ if (*str) {
+ if (*str++ != ':')
+ return -1;
+ if (!isdigit(*str))
+ return -1;
+ v = strtoul(str, &str, 10);
+ if (errno || *str || v >= 60UL)
+ return -1;
+ ret += (time_t)v;
+ }
+
+ return ret;
+}
+
+
+/**
+ * Parse and set a transition time setting
+ *
+ * @param str The transition time to parse
+ * @param start The currently specified transition start, will be updated
+ * @param end The currently specified transition end, will be updated
+ * @param key The configuration file setting being parsed,
+ * `NULL` if parsing the command line
+ * @return Normally 1, 0 if `str` is malformeda
+ */
+static int
+set_transition_time(char *str, struct setting_time *start, struct setting_time *end, const char *key)
+{
+ struct setting_time *settings[] = {start, end};
+ enum setting_source source = key ? SETTING_CONFIGFILE : SETTING_CMDLINE;
+ char *strs[ELEMSOF(settings)];
+ int i;
+
+ if (!get_strings(str, ELEMSOF(strs), strs, '-'))
+ return 0;
+
+ for (i = 0; i < (int)ELEMSOF(settings); i++) {
+ if (!strs[i] || settings[i]->source > source)
+ continue;
+ if (settings[i]->source & SETTING_CONFIGFILE) {
+ if (i == 0)
+ weprintf(_("Start value for `%s' specified multiple times in configuration file."), key);
+ else
+ weprintf(_("End value for `%s' specified multiple times in configuration file."), key);
+ }
+ settings[i]->source |= source;
+ settings[i]->value = parse_time(strs[i]);
+ if (settings[i]->value < 0)
+ return 0;
+ }
+
+ return 1;
+}
+
+
+/**
+ * Print list of available adjustment methods,
+ * and some helpful information
+ */
+static void
+print_method_list(void)
+{
+ size_t i;
+ printf(_("Available adjustment methods:\n"));
+ for (i = 0; gamma_methods[i]; i++)
+ if (gamma_methods[i]->is_available())
+ printf(" %s\n", gamma_methods[i]->name);
+
+ printf("\n");
+ printf(_("Specify colon-separated options with `-m METHOD:OPTIONS'.\n"));
+ /* TRANSLATORS: `help' must not be translated. */
+ printf(_("Try `-m METHOD:help' for help.\n"));
+}
+
+
+/**
+ * Print list of available location providers,
+ * and some helpful information
+ */
+static void
+print_provider_list(void)
+{
+ size_t i;
+ printf(_("Available location providers:\n"));
+ for (i = 0; location_providers[i]; i++)
+ printf(" %s\n", location_providers[i]->name);
+
+ printf("\n");
+ printf(_("Specify colon-separated options with `-l PROVIDER:OPTIONS'.\n"));
+ /* TRANSLATORS: `help' must not be translated. */
+ printf(_("Try `-l PROVIDER:help' for help.\n"));
+}
+
+
+/**
+ * Get adjustment method by name
+ *
+ * @param name The name of the adjustment method to return
+ * @return The adjustment method
+ */
+GCC_ONLY(__attribute__((__pure__, __returns_nonnull__)))
+static const struct gamma_method *
+find_gamma_method(const char *name)
+{
+ size_t i;
+ for (i = 0; gamma_methods[i]; i++)
+ if (!strcasecmp(name, gamma_methods[i]->name))
+ return gamma_methods[i];
+ /* TRANSLATORS: This refers to the method used to adjust colours e.g. VidMode */
+ eprintf(_("Unknown adjustment method `%s'."), name);
+}
+
+
+/**
+ * Get location provider by name
+ *
+ * @param name The name of the location provider to return
+ * @return The location provider
+ */
+GCC_ONLY(__attribute__((__pure__, __returns_nonnull__)))
+static const struct location_provider *
+find_location_provider(const char *name)
+{
+ size_t i;
+ for (i = 0; location_providers[i]; i++)
+ if (!strcasecmp(name, location_providers[i]->name))
+ return location_providers[i];
+ eprintf(_("Unknown location provider `%s'."), name);
+}
+
+
+/**
+ * Load default settings
+ *
+ * @param settings Output parameter for the settings
+ */
+static void
+load_defaults(struct settings *settings)
+{
+ memset(settings, 0, sizeof(*settings)); /* set each `.source` to `SETTING_DEFAULT` and booleans to 0 */
+
+ settings->config_file = NULL;
+ settings->scheme_type = UNSPECIFIED_SCHEME;
+
+ settings->day.temperature.value = DEFAULT_DAY_TEMPERATURE;
+ settings->day.brightness.value = DEFAULT_DAY_BRIGHTNESS;
+ settings->day.gamma.value[0] = DEFAULT_DAY_GAMMA;
+ settings->day.gamma.value[1] = DEFAULT_DAY_GAMMA;
+ settings->day.gamma.value[2] = DEFAULT_DAY_GAMMA;
+
+ settings->night.temperature.value = DEFAULT_NIGHT_TEMPERATURE;
+ settings->night.brightness.value = DEFAULT_NIGHT_BRIGHTNESS;
+ settings->night.gamma.value[0] = DEFAULT_NIGHT_GAMMA;
+ settings->night.gamma.value[1] = DEFAULT_NIGHT_GAMMA;
+ settings->night.gamma.value[2] = DEFAULT_NIGHT_GAMMA;
+
+ settings->hook_file.value = NULL;
+ settings->preserve_gamma.value = 1;
+ settings->use_fade.value = 1;
+
+ settings->elevation_high.value = DEFAULT_HIGH_ELEVATION;
+ settings->elevation_low.value = DEFAULT_LOW_ELEVATION;
+
+ settings->method = NULL;
+ settings->method_args = NULL;
+
+ settings->provider = NULL;
+ settings->provider_args = NULL;
+}
+
+
+/**
+ * Load settings from the command line
+ *
+ * @param settings The currently loaded settings, will be updated
+ * @param argc Number of command line arguments
+ * @param argv `NULL` terminated list of command line arguments,
+ * including argument zero
+ */
+static void
+load_from_cmdline(struct settings *settings, int argc, char *argv[])
+{
+ const char *provider_name;
+ char *s, *end, *value;
+
+ ARGBEGIN {
+ case 'b':
+ set_brightness(ARG(), &settings->day.brightness, &settings->night.brightness, NULL);
+ break;
+
+ case 'c':
+ settings->config_file = ARG();
+ break;
+
+ case 'D':
+ settings->disabled.source |= SETTING_CMDLINE;
+ settings->disabled.value = 0;
+ break;
+
+ case 'd':
+ settings->until_death = 1;
+ break;
+
+ case 'E':
+ settings->scheme_type = CLOCK_SCHEME;
+ break;
+
+ case 'e':
+ set_elevations(ARG(), &settings->elevation_high, &settings->elevation_low);
+ settings->scheme_type = SOLAR_SCHEME;
+ break;
+
+ case 'g':
+ set_gamma(ARG(), &settings->day.gamma, &settings->night.gamma, NULL);
+ break;
+
+ case 'H':
+ settings->hook_file.source |= SETTING_CMDLINE;
+ free(settings->hook_file.value);
+ settings->hook_file.value = ARG();
+ break;
+
+ case 'h':
+ print_help();
+ exit(0);
+
+ case 'l':
+ value = ARG();
+
+ /* Print list of providers if argument is `list' */
+ if (!strcasecmp(value, "list")) {
+ print_provider_list();
+ exit(0);
+ }
+
+ provider_name = NULL;
+
+ /* Don't save the result of strtof(); we simply want
+ to know if value can be parsed as a float. */
+ errno = 0;
+ strtof(value, &end);
+ if (!errno && *end == ':') {
+ /* Use instead as arguments to `manual'. */
+ provider_name = "manual";
+ settings->provider_args = value;
+ } else {
+ /* Split off provider arguments. */
+ s = strchr(value, ':');
+ if (s) {
+ *s++ = '\0';
+ settings->provider_args = s;
+ }
+
+ provider_name = value;
+ }
+
+ /* Lookup provider from name. */
+ settings->provider = find_location_provider(provider_name);
+
+ /* Print provider help if arg is `help'. */
+ if (settings->provider_args && !strcasecmp(settings->provider_args, "help")) {
+ settings->provider->print_help();
+ exit(0);
+ }
+ break;
+
+ case 'm':
+ value = ARG();
+
+ /* Print list of methods if argument is `list' */
+ if (!strcasecmp(value, "list")) {
+ print_method_list();
+ exit(0);
+ }
+
+ /* Split off method arguments. */
+ s = strchr(value, ':');
+ if (s) {
+ *s++ = '\0';
+ settings->method_args = s;
+ }
+
+ /* Find adjustment method by name. */
+ settings->method = find_gamma_method(value);
+
+ /* Print method help if arg is `help'. */
+ if (settings->method_args && !strcasecmp(settings->method_args, "help")) {
+ settings->method->print_help();
+ exit(0);
+ }
+ break;
+
+ case 'O':
+ mode = PROGRAM_MODE_ONE_SHOT;
+ set_temperature(ARG(), &settings->day.temperature, &settings->night.temperature, NULL);
+ break;
+
+ case 'o':
+ mode = PROGRAM_MODE_ONE_SHOT;
+ break;
+
+ case 'P':
+ settings->preserve_gamma.source |= SETTING_CMDLINE;
+ settings->preserve_gamma.value = 0;
+ break;
+
+ case 'p':
+ mode = PROGRAM_MODE_PRINT;
+ break;
+
+ case 'r':
+ settings->use_fade.source |= SETTING_CMDLINE;
+ settings->use_fade.value = 0;
+ break;
+
+ case 't':
+ set_temperature(ARG(), &settings->day.temperature, &settings->night.temperature, NULL);
+ break;
+
+ case 'V':
+ printf("%s\n", VERSION_STRING);
+ exit(0);
+
+ case 'v':
+ verbose = 1;
+ break;
+
+ case 'x':
+ mode = PROGRAM_MODE_RESET;
+ break;
+
+ default:
+ usage();
+
+ } ARGALT('+') {
+ case 'D':
+ settings->disabled.source |= SETTING_CMDLINE;
+ settings->disabled.value = 1;
+ break;
+
+ case 'E':
+ settings->scheme_type = SOLAR_SCHEME;
+ break;
+
+ case 'P':
+ settings->preserve_gamma.source |= SETTING_CMDLINE;
+ settings->preserve_gamma.value = 1;
+ break;
+
+ case 'r':
+ settings->use_fade.source |= SETTING_CMDLINE;
+ settings->use_fade.value = 1;
+ break;
+
+ default:
+ usage();
+ } ARGEND;
+
+ if (argc)
+ usage();
+}
+
+
+/**
+ * Load an setting, form the "redshift" section, from the configuration file
+ *
+ * @param settings The currently loaded settings, will be updated
+ * @param key The name of the configuration
+ * @param value The value of the configuration
+ */
+static void
+load_from_config_ini(struct settings *settings, const char *key, char *value)
+{
+ if (!strcasecmp(key, "temp") || !strcasecmp(key, "temperature")) {
+ set_temperature(value, &settings->day.temperature, &settings->night.temperature, key);
+ } else if (!strcasecmp(key, "temp-day") || !strcasecmp(key, "temperature-day")) {
+ set_temperature(value, &settings->day.temperature, NULL, key);
+ } else if (!strcasecmp(key, "temp-night") || !strcasecmp(key, "temperature-night")) {
+ set_temperature(value, NULL, &settings->night.temperature, key);
+
+ } else if (!strcasecmp(key, "brightness")) {
+ set_brightness(value, &settings->day.brightness, &settings->night.brightness, key);
+ } else if (!strcasecmp(key, "brightness-day")) {
+ set_brightness(value, &settings->day.brightness, NULL, key);
+ } else if (!strcasecmp(key, "brightness-night")) {
+ set_brightness(value, NULL, &settings->night.brightness, key);
+
+ } else if (!strcasecmp(key, "gamma")) {
+ set_gamma(value, &settings->day.gamma, &settings->night.gamma, key);
+ } else if (!strcasecmp(key, "gamma-day")) {
+ set_gamma(value, &settings->day.gamma, NULL, key);
+ } else if (!strcasecmp(key, "gamma-night")) {
+ set_gamma(value, NULL, &settings->night.gamma, key);
+
+ } else if (!strcasecmp(key, "transition")) {
+ weprintf(_("`transition' is deprecated and has been replaced with `fade'."));
+ goto set_use_fade;
+ } else if (!strcasecmp(key, "fade")) {
+ set_use_fade:
+ if (settings->use_fade.source & SETTING_CONFIGFILE)
+ weprintf(_("`fade' setting specified multiple times in configuration file."));
+ settings->use_fade.source |= SETTING_CONFIGFILE;
+ if (settings->use_fade.source <= SETTING_CONFIGFILE)
+ settings->use_fade.value = get_boolean(value, key);
+
+ } else if (!strcasecmp(key, "hook")) {
+ if (settings->hook_file.source & SETTING_CONFIGFILE)
+ weprintf(_("`hook' setting specified multiple times in configuration file."));
+ settings->hook_file.source |= SETTING_CONFIGFILE;
+ if (settings->hook_file.source <= SETTING_CONFIGFILE) {
+ free(settings->hook_file.value);
+ settings->hook_file.value = estrdup(value);
+ }
+
+ } else if (!strcasecmp(key, "preserve-gamma")) {
+ if (settings->preserve_gamma.source & SETTING_CONFIGFILE)
+ weprintf(_("`preserve-gamma' setting specified multiple times in configuration file."));
+ settings->preserve_gamma.source |= SETTING_CONFIGFILE;
+ if (settings->preserve_gamma.source <= SETTING_CONFIGFILE)
+ settings->preserve_gamma.value = get_boolean(value, key);
+
+ } else if (!strcasecmp(key, "start-disabled")) {
+ if (settings->disabled.source & SETTING_CONFIGFILE)
+ weprintf(_("`start-disabled' setting specified multiple times in configuration file."));
+ settings->disabled.source |= SETTING_CONFIGFILE;
+ if (settings->disabled.source <= SETTING_CONFIGFILE)
+ settings->disabled.value = get_boolean(value, key);
+
+ } else if (!strcasecmp(key, "elevation-high")) {
+ if (settings->elevation_high.source & SETTING_CONFIGFILE)
+ weprintf(_("`elevation-high' setting specified multiple times in configuration file."));
+ settings->elevation_high.source |= SETTING_CONFIGFILE;
+ if (settings->elevation_high.source <= SETTING_CONFIGFILE)
+ settings->elevation_high.value = checked_atof(value, key);
+
+ } else if (!strcasecmp(key, "elevation-low")) {
+ if (settings->elevation_low.source & SETTING_CONFIGFILE)
+ weprintf(_("`elevation-low' setting specified multiple times in configuration file."));
+ settings->elevation_low.source |= SETTING_CONFIGFILE;
+ if (settings->elevation_low.source <= SETTING_CONFIGFILE)
+ settings->elevation_low.value = checked_atof(value, key);
+
+ } else if (!strcasecmp(key, "dawn-time")) {
+ if (!set_transition_time(value, &settings->dawn_start, &settings->dawn_end, key))
+ eprintf(_("Malformed dawn-time setting `%s'."), value);
+
+ } else if (!strcasecmp(key, "dusk-time")) {
+ if (!set_transition_time(value, &settings->dusk_start, &settings->dusk_end, key))
+ eprintf(_("Malformed dusk-time setting `%s'."), value);
+
+ } else if (!strcasecmp(key, "adjustment-method")) {
+ if (!settings->method)
+ settings->method = find_gamma_method(value);
+
+ } else if (!strcasecmp(key, "location-provider")) {
+ if (!settings->provider)
+ settings->provider = find_location_provider(value);
+
+ } else {
+ weprintf(_("Unknown configuration setting `%s'."), key);
+ }
+}
+
+
+void
+load_settings(struct settings *settings, int argc, char *argv[])
+{
+ struct config_ini_section *section;
+ struct config_ini_setting *setting;
+ const struct time_period *first, *current;
+ const char *conf_path, *p;
+ char *conf_pathbuf, *s;
+ int i, j, n;
+ time_t duration;
+ size_t len;
+
+ /* Clear unused bit so they do not interfere with comparsion */
+ memset(&day_settings, 0, sizeof(day_settings));
+ memset(&night_settings, 0, sizeof(night_settings));
+
+ /* Load settings; some validation takes place */
+ load_defaults(settings);
+ load_from_cmdline(settings, argc, argv);
+ conf_path = config_ini_init(&settings->config, settings->config_file, &conf_pathbuf);
+ if ((section = config_ini_get_section(&settings->config, "redshift")))
+ for (setting = section->settings; setting; setting = setting->next)
+ load_from_config_ini(settings, setting->name, setting->value);
+
+ /* Further validate settings and select scheme */
+ n = !!settings->dawn_start.source + !!settings->dawn_end.source;
+ n += !!settings->dusk_start.source + !!settings->dusk_end.source;
+ if (settings->scheme_type == SOLAR_SCHEME) {
+ n = 0;
+ } else if (settings->scheme_type == CLOCK_SCHEME) {
+ if (!n)
+ eprintf(_("`-E' option used with a time-configuration configured."));
+ }
+ if (n) {
+ scheme.type = CLOCK_SCHEME;
+ if (n != 4)
+ eprintf(_("Partial time-configuration not supported!"));
+
+ if (settings->dawn_start.value >= ONE_DAY || settings->dusk_start.value >= ONE_DAY ||
+ labs((long)settings->dawn_end.value - (long)settings->dawn_start.value) > (long)ONE_DAY ||
+ labs((long)settings->dusk_end.value - (long)settings->dusk_start.value) > (long)ONE_DAY)
+ goto invalid_twilight;
+
+ /* TODO deal with edge-case where one of the twilights last 24 hour */
+ settings->dawn_end.value %= ONE_DAY;
+ settings->dusk_end.value %= ONE_DAY;
+ if (settings->dawn_start.value <= settings->dawn_end.value) {
+ if (BETWEEN(settings->dawn_start.value, settings->dusk_start.value, settings->dawn_end.value) ||
+ BETWEEN(settings->dawn_start.value, settings->dusk_end.value, settings->dawn_end.value))
+ goto invalid_twilight;
+ } else {
+ if (!WITHIN(settings->dawn_end.value, settings->dusk_start.value, settings->dawn_start.value) ||
+ !WITHIN(settings->dawn_end.value, settings->dusk_end.value, settings->dawn_start.value))
+ goto invalid_twilight;
+ }
+ }
+ if (settings->elevation_high.value < settings->elevation_low.value)
+ eprintf(_("High transition elevation cannot be lower than the low transition elevation."));
+
+ /* If resetting effects, use neutral colour settings (static scheme) and do not preserve gamma */
+ if (mode == PROGRAM_MODE_RESET) {
+ scheme.type = STATIC_SCHEME;
+ settings->preserve_gamma.value = 0;
+ day_settings = COLOUR_SETTING_NEUTRAL;
+ night_settings = COLOUR_SETTING_NEUTRAL;
+ goto settings_published;
+ }
+
+ /* Make hook file absolute if set from config file (relative to config file) */
+ if (settings->hook_file.source == SETTING_CONFIGFILE && conf_path) {
+#ifdef WINDOWS
+ /* Regular absolute path */
+ if (isalpha(settings->hook_file.value[0]) && settings->hook_file.value[1] == ':')
+ goto absolute_hook_path;
+ /* Absolute extended path or network path (relative extended paths do not exist) */
+ if (settings->hook_file.value[0] == '\\' && settings->hook_file.value[1] == '\\')
+ goto absolute_hook_path;
+ /* Path relative to root */
+ if (settings->hook_file.value[0] == '\\' || settings->hook_file.value[0] == '/') {
+ /* This is safe as we know that `conf_path` is a valid path */
+ if (isalpha(conf_path[0]) && conf_path[1] == ':') {
+ p = &conf_path[3];
+ goto base_found;
+ } else if (conf_path[0] == '\\' && conf_path[1] == '\\') {
+ p = &conf_path[2];
+ while (*p == '\\')
+ p++;
+ while (*p != '/' && *p != '\\')
+ p++;
+ goto base_found;
+ }
+ }
+#else
+ if (settings->hook_file.value[0] == '/')
+ goto absolute_hook_path;
+#endif
+ p = strrchr(conf_path, '/');
+ p = p ? &p[1] : conf_path;
+#ifdef WINDOWS
+ if (strrchr(p, '\\'))
+ p = &strrchr(p, '\\')[1];
+ base_found:
+#endif
+ len = (size_t)(p - conf_path);
+ s = emalloc(len + strlen(settings->hook_file.value) + 1U);
+ memcpy(s, conf_path, len);
+ stpcpy(&s[len], settings->hook_file.value);
+ free(settings->hook_file.value);
+ settings->hook_file.value = s;
+#ifdef WINDOWS
+ /* Used extended path is too long */
+ if (settings->hook_file.value[0] != '\\' && (len = strlen(settings->hook_file.value)) >= 260) {
+ /* We have already made sure the path is absolute, so \ prefix is always extended or network path */
+ settings->hook_file.value = erealloc(settings->hook_file.value, len + sizeof("\\\\?\\"));
+ memmove(&settings->hook_file.value[4], &settings->hook_file.value, len + 1U);
+ settings->hook_file.value[0] = '\\';
+ settings->hook_file.value[1] = '\\';
+ settings->hook_file.value[2] = '?';
+ settings->hook_file.value[3] = '\\';
+ }
+#endif
+ }
+ free(conf_pathbuf);
+absolute_hook_path:
+
+ /* Publish loaded settings */
+ if (mode == PROGRAM_MODE_ONE_SHOT && settings->until_death)
+ mode = PROGRAM_MODE_UNTIL_DEATH;
+ hook_file = settings->hook_file.value, settings->hook_file.value = NULL;
+ preserve_gamma = settings->preserve_gamma.value;
+ use_fade = settings->use_fade.value;
+ disable ^= settings->disabled.value;
+ day_settings.temperature = settings->day.temperature.value;
+ day_settings.brightness = settings->day.brightness.value;
+ day_settings.gamma[0] = settings->day.gamma.value[0];
+ day_settings.gamma[1] = settings->day.gamma.value[1];
+ day_settings.gamma[2] = settings->day.gamma.value[2];
+ night_settings.temperature = settings->night.temperature.value;
+ night_settings.brightness = settings->night.brightness.value;
+ night_settings.gamma[0] = settings->night.gamma.value[0];
+ night_settings.gamma[1] = settings->night.gamma.value[1];
+ night_settings.gamma[2] = settings->night.gamma.value[2];
+ if (!memcmp(&day_settings, &night_settings, sizeof(day_settings))) {
+ /* If the effects are the same throughout the day, do not use a transition scheme */
+ scheme.type = STATIC_SCHEME;
+ } else if (scheme.type == SOLAR_SCHEME) {
+ scheme.elevation.high = settings->elevation_high.value;
+ scheme.elevation.low = settings->elevation_low.value;
+ scheme.elevation.range = scheme.elevation.high - scheme.elevation.low;
+ } else if (scheme.type == CLOCK_SCHEME) {
+ scheme.time.periods = &scheme.time.periods_array[0];
+ scheme.time.periods_array[0].start = settings->dawn_start.value;
+ scheme.time.periods_array[0].day_level = 0.0;
+ scheme.time.periods_array[1].start = settings->dawn_end.value;
+ scheme.time.periods_array[1].day_level = 1.0;
+ scheme.time.periods_array[2].start = settings->dusk_start.value;
+ scheme.time.periods_array[2].day_level = 1.0;
+ scheme.time.periods_array[3].start = settings->dusk_end.value;
+ scheme.time.periods_array[3].day_level = 0.0;
+ for (i = 0; i < 4; i++) {
+ j = (i + 1) % 4;
+ scheme.time.periods_array[i].next = &scheme.time.periods_array[j];
+ duration = scheme.time.periods_array[j].start - scheme.time.periods_array[i].start;
+ if (duration < 0)
+ duration += ONE_DAY;
+ scheme.time.periods_array[i].diff_over_duration = scheme.time.periods_array[j].day_level;
+ scheme.time.periods_array[i].diff_over_duration -= scheme.time.periods_array[i].day_level;
+ scheme.time.periods_array[i].diff_over_duration /= duration ? (double)duration : 1.0;
+ }
+ }
+settings_published:
+
+ /* Output settings */
+ if (verbose) {
+ if (scheme.type == SOLAR_SCHEME) {
+ /* TRANSLATORS: Append degree symbols if possible. */
+ printf(_("Solar elevations: day above %.1f, night below %.1f\n"),
+ scheme.elevation.high, scheme.elevation.low);
+ } else if (scheme.type == CLOCK_SCHEME) {
+ printf(_("Schedule:\n"));
+ current = first = scheme.time.periods;
+ do {
+ printf(_(" %.2f%% day at %02u:%02u:%02u\n"),
+ current->day_level * 100, (unsigned)(current->start / 60 / 60 % 24),
+ (unsigned)(current->start / 60 % 60), (unsigned)(current->start % 60));
+ } while ((current = current->next) != first);
+ printf(_("(End of schedule)\n"));
+ }
+ printf(_("Temperatures: %luK at day, %luK at night\n"),
+ day_settings.temperature, night_settings.temperature);
+ printf(_("Brightness: %.2f:%.2f\n"), settings->day.brightness.value, settings->night.brightness.value);
+ /* TRANSLATORS: The string in parenthesis is either Daytime or Night (translated). */
+ printf(_("Gamma (%s): %.3f, %.3f, %.3f\n"), _("Daytime"),
+ day_settings.gamma[0], day_settings.gamma[1], day_settings.gamma[2]);
+ printf(_("Gamma (%s): %.3f, %.3f, %.3f\n"), _("Night"),
+ night_settings.gamma[0], night_settings.gamma[1], night_settings.gamma[2]);
+ }
+
+ return;
+
+invalid_twilight:
+ eprintf(_("Invalid dawn/dusk time configuration!"));
+}
diff --git a/redshift/config.mk b/redshift/config.mk
new file mode 100644
index 0000000..00a032c
--- /dev/null
+++ b/redshift/config.mk
@@ -0,0 +1,20 @@
+PREFIX = /usr
+MANPREFIX = $(PREFIX)/share/man
+LOCALEDIR = $(PREFIX)/share/locale
+
+PACKAGE = redshift-ng
+
+CC = c99
+
+PKGCONFIG = pkg-config
+PKGCONFIG_CFLAGS = $(PKGCONFIG) --cflags
+PKGCONFIG_LDFLAGS = $(PKGCONFIG) --libs
+
+GEOCLUE_LIBS = glib-2.0 gio-2.0
+
+LIBS_PKGCONFIG = $(GEOCLUE_LIBS) libgamma
+
+CPPFLAGS = -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_XOPEN_SOURCE=700 -D_GNU_SOURCE\
+ -DENABLE_GEOCLUE2 -DENABLE_COOPGAMMA
+CFLAGS = $$($(PKGCONFIG_CFLAGS) $(LIBS_PKGCONFIG))
+LDFLAGS = $$($(PKGCONFIG_LDFLAGS) $(LIBS_PKGCONFIG)) -lm -lcoopgamma -lred -lgeome
diff --git a/redshift/gamma-coopgamma.c b/redshift/gamma-coopgamma.c
new file mode 100644
index 0000000..f51c0ee
--- /dev/null
+++ b/redshift/gamma-coopgamma.c
@@ -0,0 +1,535 @@
+/*-
+ * 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"
+
+#include <libcoopgamma.h>
+
+#if defined(__clang__)
+# pragma clang diagnostic ignored "-Wkeyword-macro"
+#endif
+
+
+struct coopgamma_output_id {
+ char *edid;
+ size_t index;
+};
+
+
+struct coopgamma_crtc_state {
+ libcoopgamma_filter_t filter;
+ libcoopgamma_ramps_t plain_ramps;
+ size_t rampsize;
+};
+
+
+struct gamma_state {
+ libcoopgamma_context_t ctx;
+ struct coopgamma_crtc_state *crtcs;
+ size_t n_crtcs;
+ char **methods;
+ char *method;
+ char *site;
+ int64_t priority;
+ int list_outputs;
+ struct coopgamma_output_id *outputs;
+ size_t n_outputs;
+ size_t a_outputs;
+};
+
+
+struct signal_blockage {int dummy;};
+
+
+static int
+unblocked_signal(int signo, struct signal_blockage *prev)
+{
+ /* TODO */
+ (void) signo;
+ (void) prev;
+ return 0;
+}
+
+
+static int
+restore_signal_blockage(int signo, const struct signal_blockage *blockage)
+{
+ /* TODO */
+ (void) signo;
+ (void) blockage;
+ return 0;
+}
+
+
+static int
+update(struct gamma_state *state)
+{
+ size_t i;
+ for (i = 0; i < state->n_crtcs; i++)
+ libcoopgamma_set_gamma_sync(&state->crtcs[i].filter, &state->ctx);
+ return 0;
+}
+
+
+static void
+print_error(struct gamma_state *state)
+{
+ unsigned long long int ec = (unsigned long long int)state->ctx.error.number;
+ if (state->ctx.error.custom) {
+ if (ec && state->ctx.error.description) {
+ if (state->ctx.error.server_side)
+ weprintf(_("Server-side error number %llu: %s."), ec, state->ctx.error.description);
+ else
+ weprintf(_("Client-side error number %llu: %s."), ec, state->ctx.error.description);
+ } else if (ec) {
+ if (state->ctx.error.server_side)
+ weprintf(_("Server-side error number %llu."), ec);
+ else
+ weprintf(_("Client-side error number %llu."), ec);
+ } else if (state->ctx.error.description) {
+ if (state->ctx.error.server_side)
+ weprintf(_("Server-side error: %s."), state->ctx.error.description);
+ else
+ weprintf(_("Client-side error: %s."), state->ctx.error.description);
+ }
+ } else if (state->ctx.error.description) {
+ if (state->ctx.error.server_side)
+ weprintf(_("Server-side error: %s."), state->ctx.error.description);
+ else
+ weprintf(_("Client-side error: %s."), state->ctx.error.description);
+ } else {
+ if (state->ctx.error.server_side)
+ weprintf(_("Server-side error: %s."), strerror(state->ctx.error.number));
+ else
+ weprintf(_("Client-side error: %s."), strerror(state->ctx.error.number));
+ }
+}
+
+
+static int
+coopgamma_is_available(void)
+{
+ return 1;
+}
+
+
+static int
+coopgamma_create(struct gamma_state **state_out)
+{
+ struct gamma_state *state;
+ struct signal_blockage signal_blockage;
+
+ state = *state_out = ecalloc(1, sizeof(**state_out));
+
+ if (libcoopgamma_context_initialise(&state->ctx)) {
+ weprintf("libcoopgamma_context_initialise:");
+ return -1;
+ }
+
+ /* This is done this early to check if coopgamma is available */
+ if (unblocked_signal(SIGCHLD, &signal_blockage) < 0)
+ return -1;
+ state->methods = libcoopgamma_get_methods();
+ if (state->methods == NULL) {
+ weprintf("libcoopgamma_get_methods:");
+ if (restore_signal_blockage(SIGCHLD, &signal_blockage) < 0)
+ exit(1);
+ return -1;
+ }
+ if (restore_signal_blockage(SIGCHLD, &signal_blockage) < 0)
+ return -1;
+
+ state->priority = 0x0800000000000000LL;
+
+ return 0;
+}
+
+
+static int
+coopgamma_start(struct gamma_state *state)
+{
+ struct signal_blockage signal_blockage;
+ libcoopgamma_lifespan_t lifespan;
+ char** outputs;
+ size_t i, j, n_outputs;
+ int r;
+ double d;
+
+ switch (mode) {
+ case PROGRAM_MODE_RESET:
+ lifespan = LIBCOOPGAMMA_REMOVE;
+ break;
+ case PROGRAM_MODE_ONE_SHOT:
+ lifespan = LIBCOOPGAMMA_UNTIL_REMOVAL;
+ break;
+ case PROGRAM_MODE_CONTINUAL:
+ case PROGRAM_MODE_UNTIL_DEATH:
+ lifespan = LIBCOOPGAMMA_UNTIL_DEATH;
+ break;
+ default:
+ case PROGRAM_MODE_PRINT:
+ abort();
+ }
+
+ free(state->methods);
+ state->methods = NULL;
+
+ /* Connect to server */
+ if (unblocked_signal(SIGCHLD, &signal_blockage) < 0)
+ return -1;
+ if (libcoopgamma_connect(state->method, state->site, &state->ctx) < 0) {
+ if (errno)
+ weprintf("libcoopgamma_connect:");
+ else
+ weprintf(_("libcoopgamma_connect: could not start coopgamma server."));
+ if (restore_signal_blockage(SIGCHLD, &signal_blockage) < 0)
+ exit(1);
+ return -1;
+ }
+ if (restore_signal_blockage(SIGCHLD, &signal_blockage) < 0)
+ return -1;
+ free(state->method);
+ state->method = NULL;
+ free(state->site);
+ state->site = NULL;
+
+ /* Get available outputs */
+ outputs = libcoopgamma_get_crtcs_sync(&state->ctx);
+ for (n_outputs = 0; outputs[n_outputs]; n_outputs++);
+
+ /* List available output if edid=list was used */
+ if (state->list_outputs) {
+ if (!outputs) {
+ print_error(state);
+ return -1;
+ }
+ printf(_("Available outputs:\n"));
+ for (i = 0; outputs[i]; i++)
+ printf(" %s\n", outputs[i]);
+ if (ferror(stdout))
+ eprintf("printf:");
+ exit(0);
+ }
+
+ /* Translate crtc=N to edid=EDID */
+ for (i = 0; i < state->n_outputs; i++) {
+ if (state->outputs[i].edid)
+ continue;
+ if (state->outputs[i].index >= n_outputs) {
+ weprintf(_("Monitor number %zu does not exist, available monitors are [0, %zu]"),
+ state->outputs[i].index, n_outputs - 1);
+ return -1;
+ }
+ state->outputs[i].edid = estrdup(outputs[state->outputs[i].index]);
+ }
+
+ /* Use all outputs if none were specified */
+ if (state->n_outputs == 0) {
+ state->n_outputs = state->a_outputs = n_outputs;
+ state->outputs = emalloc(n_outputs * sizeof(*state->outputs));
+ for (i = 0; i < n_outputs; i++)
+ state->outputs[i].edid = estrdup(outputs[i]);
+ }
+
+ free(outputs);
+
+ /* Initialise information for each output */
+ state->crtcs = ecalloc(state->n_outputs, sizeof(*state->crtcs));
+ for (i = 0; i < state->n_outputs; i++) {
+ libcoopgamma_crtc_info_t info;
+ struct coopgamma_crtc_state *crtc = state->crtcs + state->n_crtcs;
+
+ crtc->filter.priority = state->priority;
+ crtc->filter.crtc = state->outputs[i].edid;
+ crtc->filter.lifespan = lifespan;
+#if defined(__GNUC__) && !defined(__clang__)
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wdiscarded-qualifiers"
+#endif
+ crtc->filter.class = PACKAGE "::redshift::standard";
+#if defined(__GNUC__) && !defined(__clang__)
+# pragma GCC diagnostic pop
+#endif
+
+ if (libcoopgamma_get_gamma_info_sync(crtc->filter.crtc, &info, &state->ctx) < 0) {
+ int saved_errno = errno;
+ weprintf(_("Failed to retrieve information for output `%s':\n"), outputs[i]);
+ errno = saved_errno;
+ print_error(state);
+ return -1;
+ }
+ if (!info.cooperative) {
+ weprintf(_("coopgamma is not available.\n"));
+ return -1;
+ }
+ if (info.supported == LIBCOOPGAMMA_NO) {
+ weprintf(_("Output `%s' does not support gamma adjustments, skipping."), outputs[i]);
+ continue;
+ }
+
+ /* Get total size of the ramps */
+ switch (info.depth) {
+#define X(SUFFIX, RAMPS, TYPE, MAX, DEPTH)\
+ case DEPTH:\
+ crtc->rampsize = sizeof(TYPE);\
+ break
+ LIST_RAMPS_STOP_VALUE_TYPES(X, ;);
+#undef X
+ default:
+ if (info.depth > 0) {
+ weprintf(_("output `%s' uses an unsupported depth "
+ "for its gamma ramps: %i bits, skipping\n"),
+ outputs[i], info.depth);
+ } else {
+ weprintf(_("output `%s' uses an unrecognised depth, "
+ "for its gamma ramps, with the code %i, "
+ "skipping\n"), outputs[i], info.depth);
+ }
+ continue;
+ }
+ crtc->rampsize *= info.red_size + info.green_size + info.blue_size;
+
+ crtc->filter.depth = info.depth;
+ crtc->filter.ramps.u8.red_size = info.red_size;
+ crtc->filter.ramps.u8.green_size = info.green_size;
+ crtc->filter.ramps.u8.blue_size = info.blue_size;
+ crtc->plain_ramps.u8.red_size = info.red_size;
+ crtc->plain_ramps.u8.green_size = info.green_size;
+ crtc->plain_ramps.u8.blue_size = info.blue_size;
+
+ /* Initialise plain ramp and working ramp */
+#define float f
+#define double d
+ switch (info.depth) {
+#define X(SUFFIX, RAMPS, TYPE, MAX, DEPTH)\
+ case DEPTH:\
+ r = libcoopgamma_ramps_initialise(&crtc->filter.ramps.SUFFIX);\
+ if (r < 0) {\
+ perror("libcoopgamma_ramps_initialise");\
+ return -1;\
+ }\
+ r = libcoopgamma_ramps_initialise(&crtc->plain_ramps.SUFFIX);\
+ if (r < 0) {\
+ perror("libcoopgamma_ramps_initialise");\
+ return -1;\
+ }\
+ for (j = 0; j < crtc->plain_ramps.SUFFIX.red_size; j++) {\
+ d = j;\
+ d /= crtc->plain_ramps.SUFFIX.red_size;\
+ crtc->plain_ramps.SUFFIX.red[j] = d * (MAX);\
+ }\
+ for (j = 0; j < crtc->plain_ramps.SUFFIX.green_size; j++) {\
+ d = j;\
+ d /= crtc->plain_ramps.SUFFIX.green_size;\
+ crtc->plain_ramps.SUFFIX.green[j] = d * (MAX);\
+ }\
+ for (j = 0; j < crtc->plain_ramps.SUFFIX.blue_size; j++) {\
+ d = j;\
+ d /= crtc->plain_ramps.SUFFIX.blue_size;\
+ crtc->plain_ramps.SUFFIX.blue[j] = d * (MAX);\
+ }\
+ break
+ LIST_RAMPS_STOP_VALUE_TYPES(X, ;);
+#undef X
+ default:
+ abort();
+ }
+#undef float
+#undef double
+
+ state->outputs[i].edid = NULL;
+ state->n_crtcs++;
+ }
+
+ free(state->outputs);
+ state->outputs = NULL;
+ state->n_outputs = 0;
+
+ return 0;
+}
+
+
+static void
+coopgamma_free(struct gamma_state *state)
+{
+ free(state->methods);
+ free(state->method);
+ free(state->site);
+
+ while (state->n_crtcs--) {
+ state->crtcs[state->n_crtcs].filter.class = NULL;
+ libcoopgamma_filter_destroy(&state->crtcs[state->n_crtcs].filter);
+ libcoopgamma_ramps_destroy(&state->crtcs[state->n_crtcs].plain_ramps);
+ }
+ free(state->crtcs);
+
+ libcoopgamma_context_destroy(&state->ctx, 1);
+ while (state->n_outputs--)
+ free(state->outputs[state->n_outputs].edid);
+ state->n_outputs = 0;
+ free(state->outputs);
+
+ free(state);
+}
+
+
+static void
+coopgamma_print_help(void) /* TODO not documented in readme and manpage */
+{
+ printf(_("Adjust gamma ramps with coopgamma.\n"));
+ printf("\n");
+
+ printf(" display=%s %s\n", _("NAME "), _("Display server instance to apply adjustments to"));
+ printf(" crtc=%s %s\n", _("N "), _("Index of CRTC to apply adjustments to"));
+ printf(" edid=%s %s\n", _("EDID "), _("EDID of monitor to apply adjustments to, "
+ "enter `list' to list available monitors"));
+ printf(" priority=%s %s\n", _("N "), _("The application order of the adjustments, "
+ "default value is 576460752303423488"));
+ printf(" method=%s %s\n", _("NAME "), _("Underlaying adjustment method, "
+ "enter `list' to list available methods"));
+ printf("\n");
+}
+
+
+static int
+coopgamma_set_option(struct gamma_state *state, const char *key, const char *value)
+{
+ size_t i;
+ char *end;
+ long long int priority;
+
+ if (!strcasecmp(key, "priority")) {
+ errno = 0;
+ priority = strtoll(value, &end, 10);
+ if (errno || *end || priority < INT64_MIN || priority > INT64_MAX) {
+ weprintf(_("Value of method parameter `crtc' must be a integer in [%lli, %lli]."),
+ (long long int)INT64_MIN, (long long int)INT64_MAX);
+ return -1;
+ }
+ state->priority = priority;
+ } else if (!strcasecmp(key, "method")) {
+ if (state->method != NULL) {
+ weprintf(_("Method parameter `method' can only be used once."));
+ return -1;
+ }
+ if (!strcasecmp(value, "list")) {
+ /* TRANSLATORS: coopgamma help output the word "coopgamma" must not be translated */
+ printf(_("Available adjustment methods for coopgamma:\n"));
+ for (i = 0; state->methods[i]; i++)
+ printf(" %s\n", state->methods[i]);
+ if (ferror(stdout))
+ eprintf("printf:");
+ exit(0);
+ }
+ state->method = estrdup(value);
+ } else if (!strcasecmp(key, "display")) {
+ if (state->site != NULL) {
+ weprintf(_("Method parameter `display' can only be used once."));
+ return -1;
+ }
+ state->site = estrdup(value);
+ } else if (!strcasecmp(key, "edid") || !strcasecmp(key, "crtc")) {
+ if (state->n_outputs == state->a_outputs) {
+ state->a_outputs += 8;
+ state->outputs = erealloc(state->outputs, state->a_outputs * sizeof(*state->outputs));
+ }
+ if (!strcasecmp(key, "edid")) {
+ state->outputs[state->n_outputs].edid = estrdup(value);
+ if (!strcasecmp(state->outputs[state->n_outputs].edid, "list"))
+ state->list_outputs = 1;
+ } else {
+ state->outputs[state->n_outputs].edid = NULL;
+ errno = 0;
+ state->outputs[state->n_outputs].index = (size_t)strtoul(value, &end, 10);
+ if (!*end && errno == ERANGE && state->outputs[state->n_outputs].index == SIZE_MAX) {
+ state->outputs[state->n_outputs].index = SIZE_MAX;
+ } else if (errno || *end) {
+ weprintf(_("Value of method parameter `crtc' must be a non-negative integer."));
+ return -1;
+ }
+ }
+ state->n_outputs++;
+ } else {
+ weprintf(_("Unknown method parameter: `%s'."), key);
+ return -1;
+ }
+
+ return 0;
+}
+
+
+static void
+coopgamma_restore(struct gamma_state *state)
+{
+ size_t i;
+ for (i = 0; i < state->n_crtcs; i++)
+ state->crtcs[i].filter.lifespan = LIBCOOPGAMMA_REMOVE;
+ update(state);
+ for (i = 0; i < state->n_crtcs; i++)
+ state->crtcs[i].filter.lifespan = LIBCOOPGAMMA_UNTIL_DEATH;
+}
+
+
+static int
+coopgamma_apply(struct gamma_state *state, const struct colour_setting *setting, int perserve)
+{
+ libcoopgamma_filter_t *filter;
+ libcoopgamma_filter_t *last_filter = NULL;
+ size_t i;
+
+ (void) perserve;
+
+ for (i = 0; i < state->n_crtcs; i++, last_filter = filter) {
+ filter = &state->crtcs[i].filter;
+
+ /* Copy ramps for previous CRTC if its ramps is of same size and depth */
+ if (last_filter &&
+ last_filter->ramps.u8.red_size == filter->ramps.u8.red_size &&
+ last_filter->ramps.u8.green_size == filter->ramps.u8.green_size &&
+ last_filter->ramps.u8.blue_size == filter->ramps.u8.blue_size) {
+ memcpy(filter->ramps.u8.red, last_filter->ramps.u8.red, state->crtcs[i].rampsize);
+ continue;
+ }
+
+ /* Otherwise, create calculate the ramps */
+ memcpy(filter->ramps.u8.red, state->crtcs[i].plain_ramps.u8.red, state->crtcs[i].rampsize);
+ switch (filter->depth) {
+#define X(SUFFIX, RAMPS, TYPE, MAX, DEPTH)\
+ case DEPTH:\
+ fill_ramps_##SUFFIX((void *)(filter->ramps.u8.red),\
+ (void *)(filter->ramps.u8.green),\
+ (void *)(filter->ramps.u8.blue),\
+ NULL, NULL, NULL,\
+ filter->ramps.u8.red_size,\
+ filter->ramps.u8.green_size,\
+ filter->ramps.u8.blue_size,\
+ setting);\
+ break
+ LIST_RAMPS_STOP_VALUE_TYPES(X, ;);
+#undef X
+ default:
+ abort();
+ }
+ }
+
+ return update(state);
+}
+
+
+const struct gamma_method coopgamma_gamma_method = GAMMA_METHOD_INIT("coopgamma", 1, 0, coopgamma);
diff --git a/redshift/gamma-drm.c b/redshift/gamma-drm.c
new file mode 100644
index 0000000..cecc836
--- /dev/null
+++ b/redshift/gamma-drm.c
@@ -0,0 +1,51 @@
+/*-
+ * 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"
+
+
+static int
+drm_is_available(void)
+{
+ return libgamma_is_method_available(LIBGAMMA_METHOD_LINUX_DRM);
+}
+
+
+static int
+drm_create(struct gamma_state **state_out)
+{
+ return direct_create(state_out, LIBGAMMA_METHOD_LINUX_DRM, "drm");
+}
+
+
+static void
+drm_print_help(void)
+{
+ printf(_("Adjust gamma ramps with Direct Rendering Manager.\n"));
+ printf("\n");
+ direct_print_help(LIBGAMMA_METHOD_LINUX_DRM);
+}
+
+
+#define drm_set_option direct_set_option
+#define drm_start direct_start
+#define drm_apply direct_apply
+#define drm_restore direct_restore
+#define drm_free direct_free
+const struct gamma_method drm_gamma_method = GAMMA_METHOD_INIT("drm", 0, 0, drm);
diff --git a/redshift/gamma-dummy.c b/redshift/gamma-dummy.c
new file mode 100644
index 0000000..de4b687
--- /dev/null
+++ b/redshift/gamma-dummy.c
@@ -0,0 +1,89 @@
+/*-
+ * 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"
+
+
+static int
+dummy_is_available(void)
+{
+ return 1;
+}
+
+
+static int
+dummy_create(struct gamma_state **state_out)
+{
+ *state_out = NULL;
+ return 0;
+}
+
+
+static int
+dummy_start(struct gamma_state *state)
+{
+ (void) state;
+ weprintf(_("WARNING: Using dummy gamma method! Display will not be affected by this gamma method.\n"));
+ return 0;
+}
+
+
+static void
+dummy_restore(struct gamma_state *state)
+{
+ (void) state;
+}
+
+
+static void
+dummy_free(struct gamma_state *state)
+{
+ (void) state;
+}
+
+
+static void
+dummy_print_help(void)
+{
+ printf(_("Does not affect the display but prints the color temperature to the terminal.\n"));
+ printf("\n");
+}
+
+
+static int
+dummy_set_option(struct gamma_state *state, const char *key, const char *value)
+{
+ (void) state;
+ (void) value;
+ weprintf(_("Unknown method parameter: `%s'."), key);
+ return -1;
+}
+
+
+static int
+dummy_apply(struct gamma_state *state, const struct colour_setting *setting, int preserve)
+{
+ (void) state;
+ (void) preserve;
+ printf(_("Temperature: %lu\n"), setting->temperature);
+ return 0;
+}
+
+
+const struct gamma_method dummy_gamma_method = GAMMA_METHOD_INIT("dummy", 0, 0, dummy);
diff --git a/redshift/gamma-quartz.c b/redshift/gamma-quartz.c
new file mode 100644
index 0000000..c303031
--- /dev/null
+++ b/redshift/gamma-quartz.c
@@ -0,0 +1,51 @@
+/*-
+ * 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"
+
+
+static int
+quartz_is_available(void)
+{
+ return libgamma_is_method_available(LIBGAMMA_METHOD_QUARTZ_CORE_GRAPHICS);
+}
+
+
+static int
+quartz_create(struct gamma_state **state_out)
+{
+ return direct_create(state_out, LIBGAMMA_METHOD_QUARTZ_CORE_GRAPHICS, "quartz");
+}
+
+
+static void
+quartz_print_help(void)
+{
+ printf(_("Adjust gamma ramps on macOS using Quartz.\n"));
+ printf("\n");
+ direct_print_help(LIBGAMMA_METHOD_QUARTZ_CORE_GRAPHICS);
+}
+
+
+#define quartz_set_option direct_set_option
+#define quartz_start direct_start
+#define quartz_apply direct_apply
+#define quartz_restore direct_restore
+#define quartz_free direct_free
+const struct gamma_method quartz_gamma_method = GAMMA_METHOD_INIT("quartz", 1, 1, quartz);
diff --git a/redshift/gamma-randr.c b/redshift/gamma-randr.c
new file mode 100644
index 0000000..1ccd277
--- /dev/null
+++ b/redshift/gamma-randr.c
@@ -0,0 +1,51 @@
+/*-
+ * 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"
+
+
+static int
+randr_is_available(void)
+{
+ return libgamma_is_method_available(LIBGAMMA_METHOD_X_RANDR);
+}
+
+
+static int
+randr_create(struct gamma_state **state_out)
+{
+ return direct_create(state_out, LIBGAMMA_METHOD_X_RANDR, "randr");
+}
+
+
+static void
+randr_print_help(void)
+{
+ printf(_("Adjust gamma ramps with the X RANDR extension.\n"));
+ printf("\n");
+ direct_print_help(LIBGAMMA_METHOD_X_RANDR);
+}
+
+
+#define randr_set_option direct_set_option
+#define randr_start direct_start
+#define randr_apply direct_apply
+#define randr_restore direct_restore
+#define randr_free direct_free
+const struct gamma_method randr_gamma_method = GAMMA_METHOD_INIT("randr", 1, 0, randr);
diff --git a/redshift/gamma-vidmode.c b/redshift/gamma-vidmode.c
new file mode 100644
index 0000000..5c2bdf3
--- /dev/null
+++ b/redshift/gamma-vidmode.c
@@ -0,0 +1,51 @@
+/*-
+ * 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"
+
+
+static int
+vidmode_is_available(void)
+{
+ return libgamma_is_method_available(LIBGAMMA_METHOD_X_VIDMODE);
+}
+
+
+static int
+vidmode_create(struct gamma_state **state_out)
+{
+ return direct_create(state_out, LIBGAMMA_METHOD_X_VIDMODE, "vidmode");
+}
+
+
+static void
+vidmode_print_help(void)
+{
+ printf(_("Adjust gamma ramps with the X VidMode extension.\n"));
+ printf("\n");
+ direct_print_help(LIBGAMMA_METHOD_X_VIDMODE);
+}
+
+
+#define vidmode_set_option direct_set_option
+#define vidmode_start direct_start
+#define vidmode_apply direct_apply
+#define vidmode_restore direct_restore
+#define vidmode_free direct_free
+const struct gamma_method vidmode_gamma_method = GAMMA_METHOD_INIT("vidmode", 1, 0, vidmode);
diff --git a/redshift/gamma-wingdi.c b/redshift/gamma-wingdi.c
new file mode 100644
index 0000000..6d983c5
--- /dev/null
+++ b/redshift/gamma-wingdi.c
@@ -0,0 +1,51 @@
+/*-
+ * 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"
+
+
+static int
+wingdi_is_available(void)
+{
+ return libgamma_is_method_available(LIBGAMMA_METHOD_W32_GDI);
+}
+
+
+static int
+wingdi_create(struct gamma_state **state_out)
+{
+ return direct_create(state_out, LIBGAMMA_METHOD_W32_GDI, "wingdi");
+}
+
+
+static void
+wingdi_print_help(void)
+{
+ printf(_("Adjust gamma ramps with the Windows GDI.\n"));
+ printf("\n");
+ direct_print_help(LIBGAMMA_METHOD_W32_GDI);
+}
+
+
+#define wingdi_set_option direct_set_option
+#define wingdi_start direct_start
+#define wingdi_apply direct_apply
+#define wingdi_restore direct_restore
+#define wingdi_free direct_free
+const struct gamma_method wingdi_gamma_method = GAMMA_METHOD_INIT("wingdi", 1, 0, wingdi);
diff --git a/redshift/gamma.c b/redshift/gamma.c
new file mode 100644
index 0000000..8dbb99b
--- /dev/null
+++ b/redshift/gamma.c
@@ -0,0 +1,139 @@
+/*-
+ * 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"
+
+
+const struct gamma_method *gamma_methods[] = {
+#ifdef ENABLE_COOPGAMMA
+ &coopgamma_gamma_method,
+#endif
+ &drm_gamma_method,
+ &randr_gamma_method,
+ &vidmode_gamma_method,
+ &quartz_gamma_method,
+ &wingdi_gamma_method,
+ &dummy_gamma_method,
+ NULL
+};
+
+
+/**
+ * Attempt to start a specific adjustment method
+ *
+ * @param method The adjustment method
+ * @param state_out Output parameter for the adjustment method state
+ * @param config Loaded information file
+ * @param args `NULL` or option part of the command line argument for the adjustment method
+ * @return 0 on success, -1 on failure
+ */
+static int
+try_start(const struct gamma_method *method, GAMMA_STATE **state_out, struct config_ini_state *config, char *args)
+{
+ struct config_ini_section *section;
+ struct config_ini_setting *setting;
+ char *next_arg, *value;
+ const char *key;
+
+ if (method->create(state_out) < 0) {
+ weprintf(_("Initialization of %s failed."), method->name);
+ goto fail;
+ }
+
+ /* Set method options from config file */
+ if ((section = config_ini_get_section(config, method->name)))
+ for (setting = section->settings; setting; setting = setting->next)
+ if (method->set_option(*state_out, setting->name, setting->value) < 0)
+ goto set_option_fail;
+
+ /* Set method options from command line */
+ for (; args && *args; args = next_arg) {
+ if (!strncasecmp(args, "display=", sizeof("display=") - 1U))
+ next_arg = &args[strcspn(args, ";")];
+ else
+ next_arg = &args[strcspn(args, ";:")];
+ if (*next_arg)
+ *next_arg++ = '\0';
+
+ key = args;
+ value = strchr(args, '=');
+ if (!value) {
+ weprintf(_("Failed to parse option `%s'."), args);
+ goto fail;
+ }
+ *value++ = '\0';
+
+ if (method->set_option(*state_out, key, value) < 0)
+ goto set_option_fail;
+ }
+
+ /* Start method */
+ if (method->start(*state_out) < 0) {
+ weprintf(_("Failed to start adjustment method %s."), method->name);
+ goto fail;
+ }
+
+ return 0;
+
+set_option_fail:
+ weprintf(_("Failed to set %s option."), method->name);
+ /* TRANSLATORS: `help' must not be translated. */
+ weprintf(_("Try `-m %s:help' for more information."), method->name);
+fail:
+ if (*state_out) {
+ method->free(*state_out);
+ *state_out = NULL;
+ }
+ return -1;
+}
+
+
+void
+acquire_adjustment_method(struct settings *settings, GAMMA_STATE **method_state_out)
+{
+ size_t i;
+
+ if (settings->method) {
+ /* Use method specified on command line */
+ if (try_start(settings->method, method_state_out, &settings->config, settings->method_args) < 0)
+ exit(1);
+ } else {
+ /* Try all methods, use the first that works */
+ for (i = 0; gamma_methods[i]; i++) {
+ if (!gamma_methods[i]->autostart)
+ continue;
+ if (!gamma_methods[i]->is_available())
+ continue;
+
+ if (try_start(gamma_methods[i], method_state_out, &settings->config, NULL) < 0) {
+ weprintf(_("Trying next method..."));
+ continue;
+ }
+
+ /* Found method that works */
+ printf(_("Using method `%s'.\n"), gamma_methods[i]->name);
+ settings->method = gamma_methods[i];
+ break;
+ }
+
+ /* Failure if no methods were successful at this point */
+ if (!settings->method)
+ eprintf(_("No more methods to try."));
+ }
+}
diff --git a/redshift/hooks.c b/redshift/hooks.c
new file mode 100644
index 0000000..96f6f83
--- /dev/null
+++ b/redshift/hooks.c
@@ -0,0 +1,249 @@
+/*-
+ * 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"
+
+
+/**
+ * Names of periods supplied to scripts
+ */
+static const char *period_names[] = {
+ [PERIOD_NONE] = "none",
+ [PERIOD_DAYTIME] = "daytime",
+ [PERIOD_NIGHT] = "night",
+ [PERIOD_TRANSITION] = "transition"
+};
+
+
+/**
+ * Path name of the hook directory, `NULL` if not found
+ */
+static char *dirpath = NULL;
+
+/**
+ * The allocation size of `dirpath`
+ */
+static size_t dirpathsize;
+
+/**
+ * The length of the string in `dirpath`
+ */
+static size_t dirpathlen;
+
+
+/**
+ * Paths, in order of priority, to test when looking for
+ * the hook directory for redshift
+ */
+static const struct env_path paths[] = {
+ {0, "XDG_CONFIG_HOME", "/redshift-ng/hooks"},
+ {0, "XDG_CONFIG_HOME", "/redshift/hooks"},
+#if defined(WINDOWS)
+ {0, "localappdata", "/redshift-ng/hooks"},
+ {0, "localappdata", "/redshift/hooks"},
+#endif
+ {0, "HOME", "/.config/redshift-ng/hooks"},
+ {0, "HOME", "/.config/redshift/hooks"},
+ {0, NULL, "/.config/redshift-ng/hooks"},
+ {0, NULL, "/.config/redshift/hooks"},
+ {1, "XDG_CONFIG_DIRS", "/redshift-ng/hooks"},
+ {1, "XDG_CONFIG_DIRS", "/redshift/hooks"},
+#if !defined(WINDOWS)
+ {0, "", "/etc/redshift-ng/hooks"},
+ {0, "", "/etc/redshift/hooks"}
+#endif
+};
+
+
+/**
+ * Deallocates `dirpath`
+ */
+static void
+cleanup(void)
+{
+ free(dirpath);
+ dirpath = NULL;
+}
+
+
+/**
+ * Search for and open the hook directory for reading
+ *
+ * @param path_out Output parameter for the hook directroy path
+ * @param pathbuf_out Output parameter for the memory allocation for `*path_out`;
+ * will be set to `NULL`; shall be free(3)d by the caller
+ * @return `DIR` object for the reading the directory; `NULL` if not found
+ */
+static DIR *
+open_hooks_dir(const char **path_out, char **pathbuf_out)
+{
+ DIR *dir = NULL;
+ size_t i;
+
+ *path_out = NULL;
+ *pathbuf_out = NULL;
+
+ for (i = 0; !dir && i < ELEMSOF(paths); i++)
+ dir = try_path_opendir(&paths[i], path_out, pathbuf_out);
+ if (dir)
+ weprintf(_("Found hook directory `%s'."), *path_out);
+ else
+ weprintf(_("No hook directory found."));
+
+ return dir;
+}
+
+
+/**
+ * Run hook file
+ *
+ * @param path The path to the hook file
+ * @param argv `NULL` terminated list of command line arguments
+ * for the hooks; must contain an unused initial slot,
+ * which the function will use to provide the zeroth
+ * argument
+ */
+static void
+run_hook(const char *path, const char *argv[])
+{
+#ifdef WINDOWS
+ /* TODO [Windows] hooks are not support on Windows */
+#else
+ switch (fork()) {
+ case -1:
+ weprintf("fork:");
+ break;
+
+ case 0:
+ if (dup2(STDOUT_FILENO, STDERR_FILENO) != STDERR_FILENO) {
+ weprintf("dup2 <stdout> <stderr>:");
+ _exit(1);
+ }
+ argv[0] = path;
+ execv(path, (const void *)argv);
+ if (errno != EACCES)
+ weprintf("execv %s:", path);
+ _exit(1);
+
+ default:
+ /* SIGCHLD is ignored */
+ break;
+ }
+#endif
+}
+
+
+/**
+ * Run hooks
+ *
+ * @param argv `NULL` terminated list of command line arguments
+ * for the hooks; must contain an unused initial slot,
+ * which the function will use to provide the zeroth
+ * argument
+ */
+static void
+run_hooks(const char *argv[])
+{
+ static int looked_up_dir = 0;
+ static int hook_file_is_regular = 0;
+ static int hook_file_checked = 0;
+
+ DIR *dir;
+ struct dirent *f;
+ size_t required;
+ const char *dirpath_static;
+
+ if (hook_file_is_regular) {
+ goto run_hook_file;
+
+ } else if (hook_file) {
+ if (!hook_file_checked) {
+ hook_file_checked = 1;
+ if (!strcmp(hook_file, "/dev/null") ||
+ !strcmp(hook_file, "/var/empty") ||
+ !strcmp(hook_file, "/var/empty/"))
+ goto no_hooks;
+ }
+
+ dir = opendir(hook_file);
+ if (!dir) {
+ if (errno == ENOTDIR) {
+ hook_file_is_regular = 1;
+ run_hook_file:
+ run_hook(hook_file, argv);
+ return;
+ }
+ weprintf("opendir %s:", hook_file);
+ no_hooks:
+ free(hook_file);
+ hook_file = NULL;
+ looked_up_dir = 1;
+ dirpath = NULL;
+ return;
+ }
+
+ } else if (!looked_up_dir) {
+ looked_up_dir = 1;
+ dir = open_hooks_dir(&dirpath_static, &dirpath);
+ if (!dir)
+ return;
+ if (!dirpath)
+ dirpath = estrdup(dirpath_static);
+ dirpathsize = dirpathlen = strlen(dirpath);
+ atexit(&cleanup);
+
+ } else if (dirpath) {
+ dir = opendir(dirpath);
+ if (!dir) {
+ weprintf("opendir %s:", dirpath);
+ cleanup();
+ return;
+ }
+
+ } else {
+ return;
+ }
+
+ while ((errno = 0, f = readdir(dir))) {
+ if (f->d_name[0] == '.' || !f->d_name[0] || strchr(f->d_name, '\0')[-1] == '~')
+ continue;
+
+ required = dirpathlen + sizeof("/") + strlen(f->d_name);
+ if (required > dirpathsize)
+ dirpath = erealloc(dirpath, dirpathsize = required);
+ stpcpy(stpcpy(&dirpath[dirpathlen], "/"), f->d_name);
+
+ run_hook(dirpath, argv);
+
+ dirpath[dirpathlen] = '\0';
+ }
+
+ if (errno)
+ weprintf("readdir %s:", dirpath);
+
+ closedir(dir);
+}
+
+
+void
+run_period_change_hooks(enum period prev_period, enum period period)
+{
+ const char *argv[] = {NULL, "period-changed", period_names[prev_period], period_names[period], NULL};
+ run_hooks(argv);
+}
diff --git a/redshift/location-corelocation.m b/redshift/location-corelocation.m
new file mode 100644
index 0000000..1b94137
--- /dev/null
+++ b/redshift/location-corelocation.m
@@ -0,0 +1,311 @@
+/*-
+ * 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"
+
+#import <Foundation/Foundation.h>
+#import <CoreLocation/CoreLocation.h>
+
+
+/**
+ * Location data
+ */
+struct location_data {
+ /**
+ * The user's geographical location
+ */
+ struct location location;
+
+ /**
+ * Whether the location provider is available
+ */
+ int available;
+
+ /**
+ * Whether an unrecoverable error has occurred
+ */
+ int error;
+};
+
+
+struct location_state {
+ /**
+ * Slave thread, used to receive location updates
+ */
+ NSThread *thread;
+
+ /**
+ * Read-end of piped used to send location data
+ * from the slave thread to the master thread
+ */
+ int pipe_fd_read;
+
+ /**
+ * Write-end of piped used to send location data
+ * from the slave thread to the master thread
+ */
+ int pipe_fd_write;
+
+ /**
+ * Location data available from the slave thread
+ */
+ struct location_data data;
+
+ /**
+ * Location data sent to the master thread
+ */
+ struct location_data saved_data;
+};
+
+
+@interface LocationDelegate : NSObject <CLLocationManagerDelegate>
+ @property (strong, nonatomic) CLLocationManager *locationManager;
+ @property (nonatomic) struct location_state *state;
+@end
+
+
+static void
+send_data(struct location_state *state)
+{
+ while (write(state->pipe_fd_write, &state->data, sizeof(state->data)) == -1 && errno == EINTR);
+}
+
+
+@implementation LocationDelegate;
+
+- (void)start
+{
+ CLAuthorizationStatus authStatus;
+
+ self.locationManager = [[CLLocationManager alloc] init];
+ self.locationManager.delegate = self;
+ self.locationManager.distanceFilter = 50000;
+ self.locationManager.desiredAccuracy = kCLLocationAccuracyKilometer;
+
+ authStatus = [CLLocationManager authorizationStatus];
+
+ if (authStatus != kCLAuthorizationStatusNotDetermined &&
+ authStatus != kCLAuthorizationStatusAuthorized) {
+ weprintf(_("Not authorized to obtain location from CoreLocation."));
+ [self markError];
+ } else {
+ [self.locationManager startUpdatingLocation];
+ }
+}
+
+- (void)markError
+{
+ self.state->data.error = 1;
+ send_data(self.state);
+}
+
+- (void)markUnavailable
+{
+ self.state->data.available = 0;
+ send_data(self.state);
+}
+
+- (void)locationManager:(CLLocationManager *)manager
+ didUpdateLocations:(NSArray *)locations
+{
+ CLLocation *newLocation = [locations firstObject];
+
+ self.state->data.location.lat = newLocation.coordinate.latitude;
+ self.state->data.location.lon = newLocation.coordinate.longitude;
+ self.state->data.available = 1;
+ send_data(self.state);
+}
+
+- (void)locationManager:(CLLocationManager *)manager
+ didFailWithError:(NSError *)error
+{
+ weprintf(_("Error obtaining location from CoreLocation: %s"), [[error localizedDescription] UTF8String]);
+ if ([error code] == kCLErrorDenied)
+ [self markError];
+ else
+ [self markUnavailable];
+}
+
+- (void)locationManager:(CLLocationManager *)manager
+ didChangeAuthorizationStatus:(CLAuthorizationStatus)status
+{
+ if (status == kCLAuthorizationStatusNotDetermined) {
+ weprintf(_("Waiting for authorization to obtain location..."));
+ } else if (status != kCLAuthorizationStatusAuthorized) {
+ weprintf(_("Request for location was not authorized!"));
+ [self markError];
+ }
+}
+
+@end
+
+
+// Callback when the pipe is closed.
+//
+// Stops the run loop causing the thread to end.
+static void
+pipe_close_callback(CFFileDescriptorRef fdref, CFOptionFlags callBackTypes, void *info)
+{
+ CFFileDescriptorInvalidate(fdref);
+ CFRelease(fdref);
+
+ CFRunLoopStop(CFRunLoopGetCurrent());
+}
+
+
+@interface LocationThread : NSThread
+ @property (nonatomic) struct location_state *state;
+@end
+
+
+@implementation LocationThread;
+
+// Run loop for location provider thread.
+- (void)main
+{
+ @autoreleasepool {
+ LocationDelegate *locationDelegate;
+ CFFileDescriptorRef fdref;
+ CFRunLoopSourceRef source;
+
+ locationDelegate = [[LocationDelegate alloc] init];
+ locationDelegate.state = self.state;
+
+ // Start the location delegate on the run loop in this thread.
+ [locationDelegate performSelector:@selector(start) withObject:nil afterDelay:0];
+
+ // Create a callback that is triggered when the pipe is closed. This will
+ // trigger the main loop to quit and the thread to stop.
+ fdref = CFFileDescriptorCreate(kCFAllocatorDefault, self.state->pipe_fd_write,
+ false, pipe_close_callback, NULL);
+ CFFileDescriptorEnableCallBacks(fdref, kCFFileDescriptorReadCallBack);
+ source = CFFileDescriptorCreateRunLoopSource(kCFAllocatorDefault, fdref, 0);
+ CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
+
+ // Run the loop
+ CFRunLoopRun();
+
+ close(self.state->pipe_fd_write);
+ }
+}
+
+@end
+
+
+static int
+corelocation_create(struct location_state **state_out)
+{
+ *state_out = emalloc(sizeof(**state_out));
+ return 0;
+}
+
+
+static int
+corelocation_start(struct location_state *state)
+{
+ LocationThread *thread;
+ int pipefds[2];
+
+ state->pipe_fd_read = -1;
+ state->pipe_fd_write = -1;
+
+ state->data.available = 0;
+ state->data.error = 0;
+ state->data.location.lat = 0;
+ state->data.location.lon = 0;
+ state->saved_data = state->data;
+
+ pipe_rdnonblock(pipefds);
+ state->pipe_fd_read = pipefds[0];
+ state->pipe_fd_write = pipefds[1];
+
+ send_data(state); /* TODO why? */
+
+ thread = [[LocationThread alloc] init];
+ thread.state = state;
+ [thread start];
+ state->thread = thread;
+
+ return 0;
+}
+
+
+static void
+corelocation_free(struct location_state *state)
+{
+ if (state->pipe_fd_read >= 0)
+ close(state->pipe_fd_read);
+ free(state);
+}
+
+
+static void
+corelocation_print_help(void)
+{
+ printf(_("Use the location as discovered by the Corelocation provider.\n"));
+ printf("\n");
+}
+
+
+static int
+corelocation_set_option(struct location_state *state, const char *key, const char *value)
+{
+ (void) state;
+ (void) value;
+ weprintf(_("Unknown provider parameter: `%s'."), key);
+ return -1;
+}
+
+
+static int
+corelocation_get_fd(struct location_state *state)
+{
+ return state->pipe_fd_read;
+}
+
+
+static int
+corelocation_fetch(struct location_state *state, struct location *location_out, int *available_out)
+{
+ struct location_data data;
+ ssize_t r;
+
+ for (;;) {
+ r = read(state->pipe_fd_read, &data, sizeof(data));
+ if (r == (ssize_t)sizeof(data)) {
+ state->saved_data = data;
+ } else if (r > 0) {
+ /* writes of 512 bytes or less are always atomic on pipes */
+ weprintf("read <pipe>: %s", _("Unexpected message size"));
+ } else if (!r || errno == EAGAIN) {
+ break;
+ } else if (errno != EINTR) {
+ weprintf("read <pipe>:");
+ state->saved_data.error = 1;
+ break;
+ }
+ }
+
+ *location_out = state->saved_data.location;
+ *available_out = state->saved_data.available;
+ return state->saved_data.error ? -1 : 0;
+}
+
+
+const location_provider_t corelocation_location_provider = LOCATION_PROVIDER_INIT("corelocation", corelocation);
diff --git a/redshift/location-geoclue2.c b/redshift/location-geoclue2.c
new file mode 100644
index 0000000..9eb49fe
--- /dev/null
+++ b/redshift/location-geoclue2.c
@@ -0,0 +1,451 @@
+/*-
+ * 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"
+
+#if defined(__clang__)
+# pragma clang diagnostic push
+# pragma clang diagnostic ignored "-Wreserved-identifier"
+# pragma clang diagnostic ignored "-Wreserved-macro-identifier"
+# pragma clang diagnostic ignored "-Wdocumentation-unknown-command"
+# pragma clang diagnostic ignored "-Wdocumentation"
+# pragma clang diagnostic ignored "-Wpadded"
+#endif
+#include <glib.h>
+#include <glib/gprintf.h>
+#include <gio/gio.h>
+#if defined(__clang__)
+# pragma clang diagnostic pop
+#endif
+
+
+/**
+ * D-Bus error indicating denial of access
+ */
+#define DBUS_ACCESS_ERROR "org.freedesktop.DBus.Error.AccessDenied"
+
+
+/**
+ * Location data
+ */
+struct location_data {
+ /**
+ * The user's geographical location
+ */
+ struct location location;
+
+ /**
+ * Whether the location provider is available
+ */
+ int available;
+
+ /**
+ * Whether an unrecoverable error has occurred
+ */
+ int error;
+};
+
+
+struct location_state {
+ GMainLoop *loop;
+
+ /**
+ * Slave thread, used to receive location updates
+ */
+ GThread *thread;
+
+ /**
+ * Read-end of piped used to send location data
+ * from the slave thread to the master thread
+ */
+ int pipe_fd_read;
+
+ /**
+ * Write-end of piped used to send location data
+ * from the slave thread to the master thread
+ */
+ int pipe_fd_write;
+
+ /**
+ * Location data available from the slave thread
+ */
+ struct location_data data;
+
+ /**
+ * Location data sent to the master thread
+ */
+ struct location_data saved_data;
+};
+
+
+/* Print the message explaining denial from GeoClue */
+static void
+print_denial_message(void)
+{
+ g_printerr(_(
+ "Access to the current location was denied by GeoClue!\n"
+ "Make sure that location services are enabled and that"
+ " Redshift is permitted\nto use location services."
+ " See https://github.com/jonls/redshift#faq for more\n"
+ "information.\n"));
+}
+
+
+static void
+send_data(struct location_state *state)
+{
+ while (write(state->pipe_fd_write, &state->data, sizeof(state->data)) == -1 && errno == EINTR);
+}
+
+
+/* Indicate an unrecoverable error during GeoClue2 communication */
+static void
+mark_error(struct location_state *state)
+{
+ state->data.error = 1;
+ send_data(state);
+}
+
+
+/* Handle position change callbacks */
+static void
+geoclue_client_signal_cb(GDBusProxy *client, gchar *sender_name, gchar *signal_name, GVariant *parameters, gpointer user_data)
+{
+ struct location_state *state = user_data;
+ const gchar *location_path;
+ GDBusProxy *location;
+ GError *error;
+ GVariant *lat_v, *lon_v;
+
+ (void) sender_name;
+
+ /* Only handle LocationUpdated signals */
+ if (g_strcmp0(signal_name, "LocationUpdated"))
+ return;
+
+ /* Obtain location path */
+ g_variant_get_child(parameters, 1, "&o", &location_path);
+
+ /* Obtain location */
+ error = NULL;
+ location = g_dbus_proxy_new_sync(g_dbus_proxy_get_connection(client), G_DBUS_PROXY_FLAGS_NONE,
+ NULL, "org.freedesktop.GeoClue2", location_path,
+ "org.freedesktop.GeoClue2.Location", NULL, &error);
+ if (!location) {
+ weprintf(_("Unable to obtain location: %s."), error->message);
+ g_error_free(error);
+ mark_error(state);
+ return;
+ }
+
+ /* Read location properties */
+ lat_v = g_dbus_proxy_get_cached_property(location, "Latitude");
+ state->data.location.latitude = g_variant_get_double(lat_v);
+ lon_v = g_dbus_proxy_get_cached_property(location, "Longitude");
+ state->data.location.longitude = g_variant_get_double(lon_v);
+ state->data.available = 1;
+
+ send_data(state);
+}
+
+
+/* Callback when GeoClue name appears on the bus */
+static void
+on_name_appeared(GDBusConnection *conn, const gchar *name, const gchar *name_owner, gpointer user_data)
+{
+ struct location_state *state = user_data;
+ const gchar *client_path;
+ GDBusProxy *geoclue_client;
+ GVariant *client_path_v;
+ GDBusProxy *geoclue_manager;
+ GError *error;
+ GVariant *ret_v;
+ gchar *dbus_error;
+
+ (void) name;
+ (void) name_owner;
+
+ /* Obtain GeoClue Manager */
+ error = NULL;
+ geoclue_manager = g_dbus_proxy_new_sync(conn, G_DBUS_PROXY_FLAGS_NONE, NULL,
+ "org.freedesktop.GeoClue2", "/org/freedesktop/GeoClue2/Manager",
+ "org.freedesktop.GeoClue2.Manager", NULL, &error);
+ if (!geoclue_manager) {
+ weprintf(_("Unable to obtain GeoClue Manager: %s."), error->message);
+ g_error_free(error);
+ mark_error(state);
+ return;
+ }
+
+ /* Obtain GeoClue Client path */
+ error = NULL;
+ client_path_v = g_dbus_proxy_call_sync(geoclue_manager, "GetClient", NULL,
+ G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error);
+ if (!client_path_v) {
+ weprintf(_("Unable to obtain GeoClue client path: %s."), error->message);
+ g_error_free(error);
+ g_object_unref(geoclue_manager);
+ mark_error(state);
+ return;
+ }
+
+ g_variant_get(client_path_v, "(&o)", &client_path);
+
+ /* Obtain GeoClue client */
+ error = NULL;
+ geoclue_client = g_dbus_proxy_new_sync(conn, G_DBUS_PROXY_FLAGS_NONE, NULL, "org.freedesktop.GeoClue2",
+ client_path, "org.freedesktop.GeoClue2.Client", NULL, &error);
+ if (!geoclue_client) {
+ weprintf(_("Unable to obtain GeoClue Client: %s."), error->message);
+ g_error_free(error);
+ g_variant_unref(client_path_v);
+ g_object_unref(geoclue_manager);
+ mark_error(state);
+ return;
+ }
+
+ g_variant_unref(client_path_v);
+
+ /* Set desktop id (basename of the .desktop file) */
+ error = NULL;
+ ret_v = g_dbus_proxy_call_sync(geoclue_client, "org.freedesktop.DBus.Properties.Set",
+ g_variant_new("(ssv)", "org.freedesktop.GeoClue2.Client",
+ "DesktopId", g_variant_new("s", "redshift")),
+ G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error);
+ if (!ret_v) {
+ /* Ignore this error for now. The property is not available
+ * in early versions of GeoClue2. */
+ } else {
+ g_variant_unref(ret_v);
+ }
+
+ /* Set distance threshold */
+ error = NULL;
+ ret_v = g_dbus_proxy_call_sync(geoclue_client, "org.freedesktop.DBus.Properties.Set",
+ g_variant_new("(ssv)", "org.freedesktop.GeoClue2.Client",
+ "DistanceThreshold", g_variant_new("u", 50000)),
+ G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error);
+ if (!ret_v) {
+ weprintf(_("Unable to set distance threshold: %s."), error->message);
+ g_error_free(error);
+ g_object_unref(geoclue_client);
+ g_object_unref(geoclue_manager);
+ mark_error(state);
+ return;
+ }
+
+ g_variant_unref(ret_v);
+
+ /* Attach signal callback to client */
+ g_signal_connect(geoclue_client, "g-signal", G_CALLBACK(geoclue_client_signal_cb), user_data);
+
+ /* Start GeoClue client */
+ error = NULL;
+ ret_v = g_dbus_proxy_call_sync(geoclue_client, "Start", NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error);
+ if (!ret_v) {
+ weprintf(_("Unable to start GeoClue client: %s."), error->message);
+ if (g_dbus_error_is_remote_error(error)) {
+ dbus_error = g_dbus_error_get_remote_error( error);
+ if (!g_strcmp0(dbus_error, DBUS_ACCESS_ERROR))
+ print_denial_message();
+ g_free(dbus_error);
+ }
+ g_error_free(error);
+ g_object_unref(geoclue_client);
+ g_object_unref(geoclue_manager);
+ mark_error(state);
+ return;
+ }
+
+ g_variant_unref(ret_v);
+}
+
+
+/* Callback when GeoClue disappears from the bus */
+static void
+on_name_vanished(GDBusConnection *connection, const gchar *name, gpointer user_data)
+{
+ struct location_state *state = user_data;
+
+ (void) connection;
+ (void) name;
+
+ state->data.available = 0;
+ send_data(state);
+}
+
+
+/* Callback when the pipe to the main thread is closed */
+static gboolean
+on_pipe_closed(GIOChannel *channel, GIOCondition condition, gpointer user_data)
+{
+ struct location_state *state = user_data;
+ g_main_loop_quit(state->loop);
+
+ (void) channel;
+ (void) condition;
+ return FALSE;
+}
+
+
+/* Run loop for location provider thread */
+static void *
+run_geoclue2_loop(void *state_)
+{
+ struct location_state *state = state_;
+ GMainContext *context;
+ guint watcher_id;
+ GIOChannel *pipe_channel;
+ GSource *pipe_source;
+
+ context = g_main_context_new();
+ g_main_context_push_thread_default(context);
+ state->loop = g_main_loop_new(context, FALSE);
+
+ watcher_id = g_bus_watch_name(G_BUS_TYPE_SYSTEM, "org.freedesktop.GeoClue2",
+ G_BUS_NAME_WATCHER_FLAGS_AUTO_START,
+ on_name_appeared, on_name_vanished, state, NULL);
+
+ /* Listen for closure of pipe */
+ pipe_channel = g_io_channel_unix_new(state->pipe_fd_write);
+ pipe_source = g_io_create_watch(pipe_channel, G_IO_IN | G_IO_HUP | G_IO_ERR);
+ g_source_set_callback(pipe_source, (GSourceFunc)on_pipe_closed, state, NULL);
+ g_source_attach(pipe_source, context);
+
+ g_main_loop_run(state->loop);
+
+ g_source_unref(pipe_source);
+ g_io_channel_unref(pipe_channel);
+ close(state->pipe_fd_write);
+
+ g_bus_unwatch_name(watcher_id);
+
+ g_main_loop_unref(state->loop);
+ g_main_context_unref(context);
+
+ return NULL;
+}
+
+
+static int
+geoclue2_create(struct location_state **state_out)
+{
+#if !GLIB_CHECK_VERSION(2, 35, 0)
+ g_type_init();
+#endif
+ *state_out = emalloc(sizeof(**state_out));
+ return 0;
+}
+
+
+static int
+geoclue2_start(struct location_state *state)
+{
+ int pipefds[2];
+
+ state->pipe_fd_read = -1;
+ state->pipe_fd_write = -1;
+
+ state->data.available = 0;
+ state->data.error = 0;
+ state->data.location.latitude = 0;
+ state->data.location.longitude = 0;
+ state->saved_data = state->data;
+
+ pipe_rdnonblock(pipefds);
+ state->pipe_fd_read = pipefds[0];
+ state->pipe_fd_write = pipefds[1];
+
+ send_data(state); /* TODO why? */
+
+ state->thread = g_thread_new("geoclue2", run_geoclue2_loop, state);
+
+ return 0;
+}
+
+
+static void
+geoclue2_free(struct location_state *state)
+{
+ if (state->pipe_fd_read >= 0)
+ close(state->pipe_fd_read);
+
+ /* Closing the pipe should cause the thread to exit, but it may be blocked by I/O */
+ install_forceful_exit_signal_handlers();
+ g_thread_join(state->thread);
+ state->thread = NULL;
+
+ free(state);
+}
+
+
+static void
+geoclue2_print_help(void)
+{
+ printf(_("Use the location as discovered by a GeoClue2 provider.\n"));
+ printf("\n");
+}
+
+
+static int
+geoclue2_set_option(struct location_state *state, const char *key, const char *value)
+{
+ (void) state;
+ (void) value;
+ weprintf(_("Unknown provider parameter: `%s'."), key);
+ return -1;
+}
+
+
+static int
+geoclue2_get_fd(struct location_state *state)
+{
+ return state->pipe_fd_read;
+}
+
+
+static int
+geoclue2_fetch(struct location_state *state, struct location *location_out, int *available_out)
+{
+ struct location_data data;
+ ssize_t r;
+
+ for (;;) {
+ r = read(state->pipe_fd_read, &data, sizeof(data));
+ if (r == (ssize_t)sizeof(data)) {
+ state->saved_data = data;
+ } else if (r > 0) {
+ /* writes of 512 bytes or less are always atomic on pipes */
+ weprintf("read <pipe>: %s", _("Unexpected message size"));
+ } else if (!r || errno == EAGAIN) {
+ break;
+ } else if (errno != EINTR) {
+ weprintf("read <pipe>:");
+ state->saved_data.error = 1;
+ break;
+ }
+ }
+
+ *location_out = state->saved_data.location;
+ *available_out = state->saved_data.available;
+ return state->saved_data.error ? -1 : 0;
+}
+
+
+const struct location_provider geoclue2_location_provider = LOCATION_PROVIDER_INIT("geoclue2", geoclue2);
diff --git a/redshift/location-geofile.c b/redshift/location-geofile.c
new file mode 100644
index 0000000..62d6b26
--- /dev/null
+++ b/redshift/location-geofile.c
@@ -0,0 +1,114 @@
+/*-
+ * 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"
+
+
+struct location_state {
+ /**
+ * The loaded location
+ */
+ struct location location;
+
+ /**
+ * File to read location from, `NULL` for default
+ */
+ char *file;
+};
+
+
+static int
+geofile_create(struct location_state **state_out)
+{
+ *state_out = emalloc(sizeof(**state_out));
+ (*state_out)->file = NULL;
+ return 0;
+}
+
+
+GCC_ONLY(__attribute__((__pure__)))
+static int
+geofile_start(struct location_state *state)
+{
+ struct libgeome_data data = {.requested_data = LIBGEOME_DATUM_LATITUDE | LIBGEOME_DATUM_LONGITUDE};
+ struct libgeome_context ctx;
+ int r;
+ libgeome_basic_context(&ctx, argv0);
+ r = libgeome_get_from_file(&ctx, &data, state->file);
+ free(state->file);
+ state->file = NULL;
+ if (r || data.requested_data != (LIBGEOME_DATUM_LATITUDE | LIBGEOME_DATUM_LONGITUDE))
+ return -1;
+ state->location.latitude = data.latitude;
+ state->location.longitude = data.longitude;
+ return 0;
+}
+
+
+static void
+geofile_free(struct location_state *state)
+{
+ free(state->file);
+ free(state);
+}
+
+
+static void
+geofile_print_help(void)
+{
+ printf(_("Specify location via file.\n"));
+ printf("\n");
+
+ printf(" file=%s %s\n", _("FILE "), _("File to read location from (empty for default)"));
+ printf("\n");
+}
+
+
+static int
+geofile_set_option(struct location_state *state, const char *key, const char *value)
+{
+ if (!strcasecmp(key, "file")) {
+ free(state->file);
+ state->file = *value ? estrdup(value) : NULL;
+ return 0;
+ } else {
+ weprintf(_("Unknown provider parameter: `%s'."), key);
+ return -1;
+ }
+}
+
+
+static int
+geofile_get_fd(struct location_state *state)
+{
+ (void) state;
+ return -1;
+}
+
+
+static int
+geofile_fetch(struct location_state *state, struct location *location_out, int *available_out)
+{
+ *location_out = state->location;
+ *available_out = 1;
+ return 0;
+}
+
+
+const struct location_provider geofile_location_provider = LOCATION_PROVIDER_INIT("geofile", geofile);
diff --git a/redshift/location-manual.c b/redshift/location-manual.c
new file mode 100644
index 0000000..2928f37
--- /dev/null
+++ b/redshift/location-manual.c
@@ -0,0 +1,118 @@
+/*-
+ * 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"
+
+
+struct location_state {
+ /**
+ * The specified location, any unspecified coordinate is set to NAN
+ */
+ struct location location;
+};
+
+
+static int
+manual_create(struct location_state **state_out)
+{
+ *state_out = emalloc(sizeof(**state_out));
+ (*state_out)->location.latitude = FNAN;
+ (*state_out)->location.longitude = FNAN;
+ return 0;
+}
+
+
+GCC_ONLY(__attribute__((__pure__)))
+static int
+manual_start(struct location_state *state)
+{
+ if (isnan(state->location.latitude) || isnan(state->location.longitude))
+ eprintf(_("Latitude and longitude must be set."));
+ return 0;
+}
+
+
+static void
+manual_free(struct location_state *state)
+{
+ free(state);
+}
+
+
+static void
+manual_print_help(void)
+{
+ printf(_("Specify location manually.\n"));
+ printf("\n");
+
+ /* TRANSLATORS: "N" represents "cardinal"; right-pad with spaces to preserve display width */
+ printf(" lat=%s %s\n", _("N "), _("Latitude"));
+ printf(" lon=%s %s\n", _("N "), _("Longitude"));
+ printf("\n");
+
+ printf(_("Both values are expected to be floating point numbers,\n"
+ "negative values representing west / south, respectively.\n"));
+ printf("\n");
+}
+
+
+static int
+manual_set_option(struct location_state *state, const char *key, const char *value)
+{
+ char *end;
+ double v;
+
+ errno = 0;
+ v = strtod(value, &end);
+ if (errno || *end) {
+ weprintf(_("Malformed argument."));
+ return -1;
+ }
+
+ if (!strcasecmp(key, "lat")) {
+ state->location.latitude = v;
+ } else if (!strcasecmp(key, "lon")) {
+ state->location.longitude = v;
+ } else {
+ weprintf(_("Unknown provider parameter: `%s'."), key);
+ return -1;
+ }
+
+ return 0;
+}
+
+
+static int
+manual_get_fd(struct location_state *state)
+{
+ (void) state;
+ return -1;
+}
+
+
+static int
+manual_fetch(struct location_state *state, struct location *location_out, int *available_out)
+{
+ *location_out = state->location;
+ *available_out = 1;
+ return 0;
+}
+
+
+const struct location_provider manual_location_provider = LOCATION_PROVIDER_INIT("manual", manual);
diff --git a/redshift/location-timezone.c b/redshift/location-timezone.c
new file mode 100644
index 0000000..58bf0cf
--- /dev/null
+++ b/redshift/location-timezone.c
@@ -0,0 +1,108 @@
+/*-
+ * 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"
+
+
+struct location_state {
+ /**
+ * The loaded location
+ */
+ struct location location;
+};
+
+
+static int
+timezone_create(struct location_state **state_out)
+{
+ *state_out = emalloc(sizeof(**state_out));
+ return 0;
+}
+
+
+GCC_ONLY(__attribute__((__pure__)))
+static int
+timezone_start(struct location_state *state)
+{
+ struct libgeome_data data;
+ struct libgeome_context ctx;
+ int r;
+ libgeome_basic_context(&ctx, argv0);
+
+ data.requested_data = LIBGEOME_DATUM_LATITUDE | LIBGEOME_DATUM_LONGITUDE;
+ r = libgeome_get_from_timezone(&ctx, &data);
+ if (r) {
+ data.requested_data = LIBGEOME_DATUM_LATITUDE | LIBGEOME_DATUM_LONGITUDE;
+ r = libgeome_get_from_time(&ctx, &data);
+ }
+
+ if (r || !(data.requested_data & LIBGEOME_DATUM_LONGITUDE))
+ return -1;
+ if (data.requested_data & LIBGEOME_DATUM_LATITUDE)
+ state->location.latitude = data.latitude;
+ else
+ state->location.latitude = 0;
+ state->location.longitude = data.longitude;
+ return 0;
+}
+
+
+static void
+timezone_free(struct location_state *state)
+{
+ free(state);
+}
+
+
+static void
+timezone_print_help(void)
+{
+ printf(_("Get rough location from timezone.\n"));
+ printf("\n");
+}
+
+
+static int
+timezone_set_option(struct location_state *state, const char *key, const char *value)
+{
+ (void) state;
+ (void) value;
+ weprintf(_("Unknown provider parameter: `%s'."), key);
+ return -1;
+}
+
+
+static int
+timezone_get_fd(struct location_state *state)
+{
+ (void) state;
+ return -1;
+}
+
+
+static int
+timezone_fetch(struct location_state *state, struct location *location_out, int *available_out)
+{
+ *location_out = state->location;
+ *available_out = 1;
+ return 0;
+}
+
+
+const struct location_provider timezone_location_provider = LOCATION_PROVIDER_INIT("timezone", timezone);
diff --git a/redshift/location.c b/redshift/location.c
new file mode 100644
index 0000000..5979a2d
--- /dev/null
+++ b/redshift/location.c
@@ -0,0 +1,249 @@
+/*-
+ * 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"
+
+
+const struct location_provider *location_providers[] = {
+#ifdef ENABLE_GEOCLUE2
+ &geoclue2_location_provider,
+#endif
+#ifdef ENABLE_CORELOCATION
+ &corelocation_location_provider,
+#endif
+ &manual_location_provider,
+#ifndef WINDOWS
+ &geofile_location_provider,
+ &timezone_location_provider,
+#endif
+ NULL
+};
+
+
+/**
+ * Get the current monotonic time in milliseconds
+ *
+ * @return The number of milliseconds elapsed since some arbitrary fixed time
+ */
+static long long int
+get_monotonic_millis(void)
+{
+#if defined(WINDOWS)
+ return (long long int)GetTickCount64();
+#else
+ struct timespec now;
+ if (clock_gettime(CLOCK_MONOTONIC, &now))
+ eprintf("clock_gettime CLOCK_MONOTONIC:");
+ return (long long int)now.tv_sec * 1000LL + (long long int)now.tv_nsec / 1000000LL;
+#endif
+}
+
+
+/**
+ * Attempt to start a specific location provider
+ *
+ * @param provider The location provider
+ * @param state_out Output parameter for the location provider state
+ * @param config Loaded information file
+ * @param args `NULL` or option part of the command line argument for the location provider
+ * @return 0 on success, -1 on failure
+ */
+static int
+try_start(const struct location_provider *provider, LOCATION_STATE **state_out, struct config_ini_state *config, char *args)
+{
+ const char *manual_keys[] = {"lat", "lon"};
+ struct config_ini_section *section;
+ struct config_ini_setting *setting;
+ char *next_arg, *value;
+ const char *key;
+ int i;
+
+ if (provider->create(state_out) < 0) {
+ weprintf(_("Initialization of %s failed."), provider->name);
+ goto fail;
+ }
+
+ /* Set provider options from config file */
+ if ((section = config_ini_get_section(config, provider->name)))
+ for (setting = section->settings; setting; setting = setting->next)
+ if (provider->set_option(*state_out, setting->name, setting->value) < 0)
+ goto set_option_fail;
+
+ /* Set provider options from command line */
+ for (i = 0; args && *args; i++, args = next_arg) {
+ next_arg = &args[strcspn(args, ";:")];
+ if (*next_arg)
+ *next_arg++ = '\0';
+
+ key = args;
+ value = strchr(args, '=');
+ if (!value) {
+ /* The options for the "manual" method can be set
+ * without keys on the command line for convencience
+ * and for backwards compatability. We add the proper
+ * keys here before calling set_option(). */
+ if (!strcmp(provider->name, "manual") && i < (int)ELEMSOF(manual_keys)) {
+ key = manual_keys[i];
+ value = args;
+ } else {
+ weprintf(_("Failed to parse option `%s'."), args);
+ goto fail;
+ }
+ } else {
+ *value++ = '\0';
+ }
+
+ if (provider->set_option(*state_out, key, value) < 0)
+ goto set_option_fail;
+ }
+
+ /* Start provider */
+ if (provider->start(*state_out) < 0) {
+ weprintf(_("Failed to start provider %s."), provider->name);
+ goto fail;
+ }
+
+ return 0;
+
+set_option_fail:
+ weprintf(_("Failed to set %s option."), provider->name);
+ /* TRANSLATORS: `help' must not be translated. */
+ weprintf(_("Try `-l %s:help' for more information."), provider->name);
+fail:
+ if (*state_out) {
+ provider->free(*state_out);
+ *state_out = NULL;
+ }
+ return -1;
+}
+
+
+int
+get_location(const struct location_provider *provider, LOCATION_STATE *state, int timeout, struct location *location_out)
+{
+#ifdef WINDOWS /* we don't have poll on Windows, but neither do with have any dynamic location providers */
+ int available;
+ return provider->fetch(state, location_out, &available) < 0 ? -1 : available;
+
+#else
+ int r, available = 0;
+ struct pollfd pollfds[1];
+ long long int now = get_monotonic_millis();
+ long long int end = now + (long long int)timeout;
+
+ do {
+ pollfds[0].fd = provider->get_fd(state);
+ if (pollfds[0].fd >= 0) {
+ /* Poll on file descriptor until ready */
+ pollfds[0].events = POLLIN;
+ timeout = (int)MAX(end - now, 0);
+ r = poll(pollfds, 1, timeout);
+ if (r > 0) {
+ now = get_monotonic_millis();
+ } else if (r < 0) {
+#ifndef WINDOWS
+ if (errno == EINTR)
+ continue;
+#endif
+ weprintf("poll {{.fd=<location provider>, .events=EPOLLIN}} 1 %i:", timeout);
+ return -1;
+ } else {
+ return 0;
+ }
+ }
+
+ if (provider->fetch(state, location_out, &available) < 0)
+ return -1;
+ } while (!available && !exiting);
+
+ if (exiting)
+ eprintf(_("Terminated by user."));
+
+ return 1;
+#endif
+}
+
+
+void
+acquire_location_provider(struct settings *settings, LOCATION_STATE **location_state_out)
+{
+ size_t i;
+
+ if (settings->provider) {
+ /* Use provider specified on command line */
+ if (try_start(settings->provider, location_state_out, &settings->config, settings->provider_args) < 0)
+ exit(1);
+ } else {
+ /* Try all providers, use the first that works */
+ for (i = 0; location_providers[i]; i++) {
+ weprintf(_("Trying location provider `%s'..."), location_providers[i]->name);
+ if (try_start(location_providers[i], location_state_out, &settings->config, NULL) < 0) {
+ weprintf(_("Trying next provider..."));
+ continue;
+ }
+
+ /* Found provider that works */
+ printf(_("Using provider `%s'.\n"), location_providers[i]->name);
+ settings->provider = location_providers[i];
+ break;
+ }
+
+ /* Failure if no providers were successful at this point */
+ if (!settings->provider)
+ eprintf(_("No more location providers to try."));
+ }
+}
+
+
+int
+location_is_valid(const struct location *location)
+{
+ if (!WITHIN(MIN_LATITUDE, location->latitude, MAX_LATITUDE)) {
+ /* TRANSLATORS: Append degree symbols if possible. */
+ weprintf(_("Latitude must be between %.1f and %.1f."), MIN_LATITUDE, MAX_LATITUDE);
+ return 0;
+ }
+ if (!WITHIN(MIN_LONGITUDE, location->longitude, MAX_LONGITUDE)) {
+ /* TRANSLATORS: Append degree symbols if possible. */
+ weprintf(_("Longitude must be between %.1f and %.1f."), MIN_LONGITUDE, MAX_LONGITUDE);
+ return 0;
+ }
+ return 1;
+}
+
+
+void
+print_location(const struct location *location)
+{
+ /* TRANSLATORS: Abbreviation for `north' */
+ const char *north = _("N");
+ /* TRANSLATORS: Abbreviation for `south' */
+ const char *south = _("S");
+ /* TRANSLATORS: Abbreviation for `east' */
+ const char *east = _("E");
+ /* TRANSLATORS: Abbreviation for `west' */
+ const char *west = _("W");
+
+ /* TRANSLATORS: Append degree symbols after %f if possible.
+ * The string following each number is an abreviation for
+ * north, source, east or west (N, S, E, W). */
+ printf(_("Location: %.2f %s, %.2f %s\n"),
+ fabs(location->latitude), signbit(location->latitude) ? south : north,
+ fabs(location->longitude), signbit(location->longitude) ? west : east);
+}
diff --git a/redshift/redshift.1 b/redshift/redshift.1
new file mode 100644
index 0000000..33706fc
--- /dev/null
+++ b/redshift/redshift.1
@@ -0,0 +1,1379 @@
+.TH REDSHIFT 1 REDSHIFT-NG
+.SH NAME
+redshift \- Automatically adjust display colour temperature according the Sun
+
+.SH SYNOPSIS
+.B redshift
+[-b
+.IR brightness ]
+[-c
+.IR config-file ]
+[-D | +D] [-E | +E | -e
+.IR elevations ]
+[-g
+.IR gamma ]
+[-H
+.IR hook-file ]
+[-l
+.IB latitude : longitude
+| -l
+.IR provider [\fB:\fP options ]]
+[-m
+.IR method [\fB:\fP options ]
+[-P | +P] [-r | +r] [-dv]
+[-O
+.I temperature
+| -o | -p | -t
+.I temperature
+| -x] | -h | -V
+
+.SH DESCRIPTION
+.B redshift
+adjusts the colour temperature of your screen according to your
+surroundings. This may help your eyes hurt less or reduce the risk for
+delayed sleep phase syndrome if you are working in front of the screen
+at night.
+.PP
+The colour temperature is set according the the position of the Sun.
+A different colour temperature is set during the night and during the
+day. During dawn and early morning, the colour temperature transitions
+smoothly from night- to day-time temperature to allow your eyes to
+slowly adapt over a period of about an hour. At night, the colour
+temperature should be set to match the maps in your room. This is
+typically a low temperature at around 3000K-4000K (default is 4500K).
+During the day, the colour temperature should match the light from
+outside. Typically around 5500K-6500K (default is 6500K). The light has
+a lower temperature on an overcast day.
+.PP
+In addition to the command-line tool
+.BR redshift ,
+the GUI
+.BR redshift-gtk (1)
+provides an alternative interface that shows up as a notification icon
+in the desktop environment.
+
+.SH OPTIONS
+The following options are supported:
+.TP
+.BI -b\fR\ brightness
+Synonym for
+.B -b
+.IB brightness : brightness\fR.
+.TP
+.BI -b\fR\ day : night
+Monitor whitepoint brightness to apply at daytime and at
+nighttime. (Default: 1:1)
+
+The values most be between 0.1 and 1.0.
+
+.I day
+or
+.I night
+may be omitted, to keep unmodified, however
+at least one must be specified.
+.TP
+.BI -c\fR\ config-file
+Load settings from specified configuration file.
+
+.B /dev/null
+can be used to tell
+.B redshift
+not to load the configuration file.
+
+If
+.RB \(dq - \(dq,
+the standard input will be used.
+.TP
+.B -D
+Start in enabled state. (Default)
+.TP
+.B +D
+Start in disabled state.
+
+Ignored in one-shot mode.
+.TP
+.B -d
+Keep the process alive and remove the colour effects
+when killed.
+
+Ignored for
+.B -p
+and
+.BR -x ;
+always active for
+.B -t
+and the
+.B quartz
+adjustment method.
+.TP
+.B -E
+Use wall-clock based schedule.
+.TP
+.B +E
+Use solar elevation based schedule.
+.TP
+.BI -e\fR\ elevations
+Synonym for
+.B -e
+.IB elevations : elevations\fR.
+.TP
+.BI -e\fR\ elevation-high : elevation-low
+Sets the lowest solar elevation during daytime to
+.I elevation-high
+and the higest solar elevation during nighttime to
+.IR elevation-low .
+
+The value should be formatted as real, decimal
+values measured in degrees. Each value shall be
+formatted as one complete value, without unit
+suffix, and not split into degrees, minutes, and
+seconds. Positive values are above the horizon
+and negative values are below the horizon.
+
+.I elevation-high
+or
+.I elevation-low
+may be omitted, to keep unmodified, however at least
+one must be specified.
+
+Implies
+.BR +E .
+.TP
+.BI -g\fR\ gamma
+Synonym for
+.B -g
+.IB gamma : gamma\fR.
+.TP
+.BI -g\fR\ day : night
+Synonym for
+.B -g
+.IB day : day : day : night : night : night\fR.
+
+However, if
+.I day
+is omitted, it is a synonym for
+.B -g
+.BI : night : night : night\fR,
+or if
+.I night
+is omitted, it is a synonym for
+.B -g
+.IB day : day : day :\fR.
+.TP
+.BI -g\fR\ red : green : blue
+Synonym for
+.B -g
+.IB red : green : blue : red : green : blue\fR.
+.TP
+.BI -g\fR\ day-r : day-g : day-b : night-r : night-g : night-b
+Additional gamma correction to apply at daytime and
+at nighttime. (Default: 1:1:1:1:1:1)
+
+The values most be between 0.1 and 10.0.
+
+.IB day-r : day-g : day-b
+or
+.IB night-r : night-g : night-b
+may be omitted,
+to keep unmodified, however at least one set must be specified.
+Individual components of one set cannot be omitted, either
+nothing is omitted or an entire set, including its two colons
+.RB ( : )
+are omitted.
+.TP
+.BI -H\fR\ hook-file
+Select hook file or directory.
+
+.B /dev/null
+or
+.B /var/empty
+can be used to tell redshift not to run hook files.
+.TP
+.B -h
+Display help message.
+.TP
+.BI -l\fR\ latitude : longitude
+Your current location, in degrees. Shall be formatted a single
+real number, rather than split into integer degrees, minutes
+and seconds. The location should be specified using the GPS
+coordinate system.
+.TP
+.BI -l\fR\ provider\fR[ : options\fR]
+Select provider for automatic location updates.
+
+.I options
+is a colon-
+.RB ( : )
+and semicolon-separated
+.RB ( ; )
+list. Each option an option name and value
+separated by an equals sign
+.RB ( = ).
+
+Use
+.B -l list
+to see available providers.
+
+Use
+.BI -l\ provider :help
+to see available options,
+or refer to the
+.B EXTENDED DESCRIPTION
+section.
+.TP
+.BI -m\fR\ method\fR[ : options\fR]
+Method to use to set colour temperature.
+
+.I options
+is a colon-
+.RB ( : )
+and semicolon-separated
+.RB ( ; )
+list. Each option an option name and value
+separated by an equals sign
+.RB ( = ).
+
+Use
+.B -m list
+to see available methods.
+
+Use
+.BI -m\ method :help
+to see available options,
+or refer to the
+.B EXTENDED DESCRIPTION
+section.
+.TP
+.BI -O\ temperature
+This is a synonym for
+.B -O
+.IB temperature : temperature\fR.
+.TP
+.BI -O\ day : night
+One-shot manual mode (set colour temperature). The colour set
+is interpolated between day and night depending on the Sun's
+elevation or the clock time (depending on which
+.B redshift
+is configured to use).
+
+Values must be at least 1000 and integral.
+
+Use this with the
+.B -P
+option to clear the existing gamma ramps
+before applying the new color temperature.
+
+This is a synonym for
+.B -t
+.IB day : night
+.BR -o .
+.TP
+.B -o
+One-shot mode (do not continuously adjust colour temperature).
+
+Use this with the
+.B -P
+option to clear the existing gamma ramps
+before applying the new color temperature.
+.TP
+.B -P
+Reset exiting gamma ramps before applying new colour effects.
+.TP
+.B +P
+Preserve preexisting gamma adjustments. (Default)
+.TP
+.B -p
+Print parameters and exit.
+.TP
+.B -r
+Disable fading between colour temperatures.
+.TP
+.B +r
+Enable fading between colour temperatures. (Default)
+.TP
+.BI -t\fR\ temperature
+This is a synonym for
+.B -t
+.IB temperature : temperature\fR.
+.TP
+.BI -t\fR\ day : night
+Colour temperature to set at daytime and at nighttime.
+
+Values must be at least 1000 and integral.
+
+The value 6500 is equivalent to no colour temperature
+adjustment.
+
+Default mode, but default values may change between
+versions.
+.TP
+.B -V
+Show program implementation and version.
+.TP
+.B -v
+Enable verbose output.
+.TP
+.B -x
+Remove adjustments from screen.
+.PP
+For mutually exclusive options or options specified multiple times,
+the last specified takes effect, except the first specified option
+that outputs text (except
+.BR -p )
+is used. However, if the daytime
+alue or nighttime value is omitted for an option, the last previously
+pecified value will be used; that is, for example,
+.B -t 5000:
+and
+.B -t :3000
+do not override each other, but
+.B -t 5000:
+overrides, if specified later,
+.B 6000
+but not
+.B 3000
+in
+.BR "-t 6000:3000" .
+.PP
+Options in the command line override settings from the configuration
+file.
+
+.SH OPERANDS
+None.
+
+.SH STDIN
+Not used.
+
+.SH INPUT FILES
+None.
+
+.SH ASYNCHRONOUS EVENTS
+.B redshift
+takes the standard action for all signals except:
+.TP
+.B SIGINT
+.TQ
+.B SIGTERM
+.TQ
+.B SIGQUIT
+Smoothly disable the effects of
+.B redshift
+and terminate the
+process. If already sent, immediately disable the effects
+and terminate the process.
+.TP
+.B SIGUSR1
+Disable the effects of
+.BR redshift ,
+or if already disabled, reenable them.
+.TP
+.BR SIGUSR2 \ with\ value\ \fI0\fR
+Normally signals may be processed out of order, however
+when this signal is received,
+.B SIGUSR2
+will be blocked until all pending
+.B SIGUSR2
+signals has been processed, creating signal processing
+order barrier. This is useful when mixing
+.B SIGUSR2
+value
+.I 3
+(reloading configuration file) with other configuration changing
+.I SIGUSR2
+values.
+.TP
+.BR SIGUSR2 \ with\ value\ \fI1\fR
+Disable the effects of
+.BR redshift .
+.TP
+.BR SIGUSR2 \ with\ value\ \fI2\fR
+Enable the effects of
+.BR redshift .
+.TP
+.BR SIGUSR2 \ with\ value\ \fI3\fR
+Reload the configuration file.
+
+Settings from the command line will be overriden.
+.TP
+.BR SIGUSR2 \ with\ value\ \fI4\fR
+Execute into the currently installed version of
+.BR redshift .
+
+Only available on Linux.
+.TP
+.BR SIGUSR2 \ with\ value\ \fI5\fR
+Set the
+.B fade
+setting to off.
+.TP
+.BR SIGUSR2 \ with\ value\ \fI6\fR
+Set the
+.B fade
+setting to on.
+.TP
+.BR SIGUSR2 \ with\ value\ \fI7\fR
+Set the
+.B preserve-gamma
+setting to off.
+.TP
+.BR SIGUSR2 \ with\ value\ \fI8\fR
+Set the
+.B preserve-gamma
+setting to on.
+.TP
+.BR SIGUSR2 \ with\ value\ \fI9\fR
+Exit the process without removing the its effects.
+If the used adjustment method does not support leaving
+the effects, they will be removed.
+.TP
+.BR SIGUSR2 \ with\ value\ \fI10\fR
+Do not terminate
+.B redshift
+the standard output and standard error are closed.
+.TP
+.BR SIGUSR2 \ with\ value\ \fI11\fR
+Enable verbose mode. (The
+.B -v
+option will be treated as specified.)
+.TP
+.BR SIGUSR2 \ with\ value\ \fI12\fR
+Disable verbose mode.
+
+Ignore if started in verbose mode
+.RB ( -v
+option).
+.TP
+.BR SIGUSR2 " with other values or no value"
+Ignored.
+
+.SH STDOUT
+The standard output is used to print state information and requested
+help information. The output is subject to localisation, and the
+following formats apply for the
+.RB \(dq C \(dq
+locale. Applications taking use of this information must make sure
+to set the message locale to
+.RB \(dq C \(dq.
+For floating-point values
+.RB (\(dq %f \(dq)
+the precision is not documented as it may change between versions and
+applications should not expect any particular precision to be used.
+.PP
+When
+.B -m list
+is specified, the available gamma ramp adjustment methods
+are printed with the header
+.B \(dqAvailable adjustment methods:\en\(dq
+followed by the list in the format
+.RS
+.nf
+
+\fB\(dq%s%s\en\(dq, \fI<arbitrary whitespace>\fP, \fP<method name>\fR.
+
+.fi
+.RE
+.PP
+The list is terminated by an empty line. Additional information for
+human users is printed after the empty line.
+.PP
+When
+.B -l list
+is specified, the available location providers are
+printed with the header
+.B \(dqAvailable location providers:\en\(dq
+followed by the list in the format
+.RS
+.nf
+
+\fB\(dq%s%s\en\(dq, \fI<arbitrary whitespace>\fP, \fP<provider name>\fR.
+
+.fi
+.RE
+.PP
+The list is terminated by an empty line. Additional information for
+human users is printed after the empty line.
+.PP
+When
+.B list
+is specified for the
+.B edid
+suboption to
+.BR -m ,
+a list of available monitors will be printed to the standard output,
+with the header
+.BR "\(dqAvailable outputs:\en\(dq" ,
+in the format
+.RS
+.nf
+
+\fB\(dq%s%s\en\(dq, \fI<arbitrary whitespace>\fP, \fP<monitor identifier>\fR.
+
+.fi
+.RE
+.PP
+When
+.BR "-m method:help" ,
+.BR "-l provider:help" ,
+or
+.B -h
+is specified help information is printed on in unspecified format,
+intended only for human users.
+.PP
+When
+.B -V
+is specified, the used version of the program is printed to
+the standard output in the format
+.RS
+.nf
+
+\fB\(dq%s %s\en\(dq, \fI<implementation name>\fP, \fP<version number>\fR.
+
+.fi
+.RE
+.PP
+If
+.B -v
+is specified and the colour settings depend on the Sun's
+elevation, the elevation thresholds are printed to the standard
+output in the format
+.RS
+.nf
+
+\fB\(dqSolar elevations: day above %f, night below %f\en\(dq,\fR
+\fI<minimum solar elevation at daytime>\fB,\fR
+\fI<maximum solar elevation at nighttime>\fR.
+
+.fi
+.RE
+This line may be printed, if
+.B -v
+is specified, if
+.B redshift
+is configured.
+.PP
+If
+.B -v
+is specified and the colour settings depend on the clock time,
+the time schedule is printed to the standard output, with the header
+.B \(dqSchedule:\en\(dq
+and the footer
+.BR "\(dq(End of schedule)\e\n\(dq" ,
+in the format
+.RS
+.nf
+
+\fB\(dq%s%f%% day at %02u:%02u:%02u\en\(dq, \fI<arbitrary whitespace>\fP,\fR
+\fI<dayness level (0-100)>\fB, \fP<start hour (0-23)>\fP,\fR
+\fI<start minute (0-59)>\fB, \fP<start second (0-59)>\fR.
+
+.fi
+.RE
+These lines may be printed, if
+.B -v
+is specified, if
+.B redshift
+is configured.
+.PP
+If
+.B -v
+is specified, the colour settings is printed to the standard
+output in the format
+.RS
+.nf
+
+\fB\(dqTemperatures: %luK at day, %luK at night\en\(dq\fR
+\fB\(dqBrightness: %f:%f\en\(dq\fR
+\fB\(dqGamma (Daytime): %f, %f, %f\en\(dq\fR
+\fB\(dqGamma (Night): %f, %f, %f\en\(dq,\fR
+\fI<daytime colour temperature>\fB, \fP<nighttime temperature>\fP,\fR
+\fI<daytime whitepoint brightness>\fB, \fP<nighttime brightness>\fP,\fR
+\fI<daytime red gamma>\fB, \fP<daytime green gamma>\fP,\fR
+\fI<daytime blue gamma>\fB, \fP<nighttime red gamma>\fP,\fR
+\fI<nighttime green gamma>\fB, \fP<nighttime blue gamma>\fR.
+
+.fi
+.RE
+Each line may be printed, if
+.B -v
+is specified, if
+.B redshift
+is configured.
+.PP
+If the colour effects depend on the Sun's elevation, the user's
+geographical location will printed to the standard output in the
+format
+.RS
+.nf
+
+\fB\(dqLocation: %f %c, %f %c\en\(dq,\fR
+\fIfabs(<GPS latitude>)\fB, \fPsignbit(<GPS latitude>) ? 'S' : 'N'\fP,\fR
+\fIfabs(<GPS longitude>)\fB, \fPsignbit(<GPS longitude>) ? 'W' : 'E'\fR.
+
+.fi
+.RE
+This message is printed when the program starts and any time the
+location is updated.
+.PP
+If the colour effects are non-static, the current period of the day
+(which determine the colour effects) is printed to standard output, if
+.B -v
+or
+.B -p
+is specified, in the format
+.RS
+.nf
+
+\fB\(dqPeriod: %s\en\(dq, \fI<period>\fR
+
+.fi
+.RE
+where
+.I <period>
+is
+.BR None ,
+.BR Daytime ,
+or
+.BR Night ,
+or in the format
+.RS
+.nf
+
+\fB\(dqPeriod: Transition (%f%% day)\en\(dq, \fI<dayness level> * 100\fR.
+
+.fi
+.RE
+.I <dayness level>
+is exclusively between 0 (night) and 1 (daytime).
+.PP
+This message is printed when the program starts and any time it
+changes (if
+.B -v
+is specified).
+.PP
+If
+.B -v
+or
+.B -p
+is specified, the colour settings are printed to the
+standard output when the program standard and any time it changes
+(fade effect is ignored). These are printed in three different
+messages and, on chagne, only the settings that changed are printed:
+.RS
+.nf
+
+\fB\(dqColor temperature: %luK\en\(dq, \fI<colour temperature>\fR;
+
+\fB\(dqBrightness: %f\en\(dq, \fI<whitepoint brightness level (0-1)>\fR;
+
+\fB\(dqGamma: %f, %f, %f\en\(dq, \fI<red gamma>\fP, \fP<green gamma>\fP, \fP<blue gamma>\fR.
+
+.fi
+.RE
+.PP
+If
+.B -v
+is specified, and if running in continual mode, the program will print
+.B \(dqStatus: Enabled\en\(dq
+if starting in or when entering enabled mode, and
+.B \(dqStatus: Disabled\en\(dq
+if starting in or when entering disabled mode.
+.PP
+If
+.B -v
+is specified, and if running in continual mode, the program will print
+.B \(dqFade: Enabled\en\(dq
+or
+.B \(dqFade: Disabled\en\(dq
+to indicate whether the
+.B fade
+setting is enabled, when the program starts and when the
+setting is modified.
+.PP
+If
+.B -v
+is specified, and if running in continual mode, the program will print
+.B \(dqPreserve gamma: Enabled\en\(dq
+or
+.B \(dqPreserve gamma: Disabled\en\(dq
+to indicate whether the
+.B preserve-gamma
+setting is enabled, when the
+program starts and when the setting is modified.
+.PP
+If the
+.B dummy
+gamma ramp adjustment method is used, any time a colour
+change is applied (including each fade step), the colour temperature
+is output, for debugging purposes (brightness and gamma are not printed),
+to the standard output in the format
+.RS
+.nf
+
+\fB\(dqTemperature: %lu\en\(dq, \fI<colour temperature>\fR.
+.fi
+.RE
+
+.SH STDERR
+Default.
+
+.SH OUTPUT FILES
+None.
+
+.SH FILES
+Unless the
+.B -c
+option is used,
+.B redshift
+will look for its configuration
+file, and if found, load it. When searching for the configuration file,
+.B redshift
+will load the first found file. It will primary look for
+.IR redshift-ng/redshift.conf ,
+secondarily for
+.IR redshift/redshift.conf ,
+and tertiarily for
+.IR redshift.conf ,
+in each directory it searches. It
+will search the following directories in order: the directory set in
+the environment variable
+.IR XDG_CONFIG_HOME ,
+the directory set in the
+environment variable
+.I localappdata
+(Windows only), the
+.I .config
+directory inside directory set in the environment variable
+.IR HOME ,
+and the
+.I .config
+directory inside the user's home directory. For the two
+latter, it will quaternarily look for the file
+.IR .redshift.conf .
+If not found, it will also look for the file in each directory listed in the
+environment variable
+.I XDG_CONFIG_DIRS
+(delimited by colon
+.RB ( : )
+on Unix-like systems and by semicolon
+.RB ( ; )
+on Windows), however it try each
+directory before moving on to then next filename option. Lastly, on
+Unix-like systems, it will look for the file in
+.IR /etc .
+This means that the preferred location for the configuration file is
+.IB ${XDG_CONFIG_HOME} /redshift-ng/redshift.conf\fR.
+.PP
+.B redshift
+will use the same pattern to find the hook directory, and the
+tested subdirectories for each search directory are
+.I /redshift-ng/hooks
+and secondarily
+.IR /redshift/hooks .
+All executable files, in the found
+directory, that are neither prefixed with a period
+.BR ( . )
+or suffixed with a tilde
+.RB ( ~ )
+will be used as hooks scripts, and will be executed in
+arbitrary order. Subdirectories are not search for executable files.
+This means that the preferred location for the hook scripts is (directly
+inside)
+.IB ${XDG_CONFIG_HOME} /redshift-ng/hooks/\fR.
+
+.SH EXTENDED DESCRIPTION
+.SS Configuration file
+The configuration file uses the standard INI format. General program
+options are placed under the
+.B redshift
+header
+.RB ( [redshift] ),
+while options for location providers are adjustment methods are
+placed under a hader with the name of that proivder or method.
+.PP
+General options are:
+.TP
+.BI temp\fR\ =\ temperature
+.TQ
+.BI temperature\fR\ =\ temperature
+Set temperature for daytime and nighttime. The value shall be
+format in the same way as with the
+.B -O
+and
+.B -t
+options.
+.TP
+.BI temp-day\fR\ =\ integer
+.TQ
+.BI temperature-day\fR\ =\ integer
+Set temperature for daytime. That value shall be an integer no
+less than 1000 (Kelvin).
+.TP
+.BI temp-night\fR\ =\ integer
+.TQ
+.BI temperature-night\fR\ =\ integer
+Set temperature for nighttime. That value shall be an integer
+no less than 1000 (Kelvin).
+.TP
+.BI brightness\fR\ =\ brightness
+Set whitepoint brightness for daytime and nighttime. The value
+shall be format in the same way as with the
+.B -b
+option.
+.TP
+.BI brightness-day\fR\ =\ 0.1-1.0
+Set whitepoint brightness for daytime. That value shall be an
+within [0.1, 1.0].
+.TP
+.BI brightness-night\fR\ =\ 0.1-1.0
+Set whitepoint brightness for nighttime. That value shall be an
+within [0.1, 1.0].
+.TP
+.BI gamma\fR\ =\ gamma
+Set gamma correction for daytime and nighttime. The value shall
+be format in the same way as with the
+.B -g
+option.
+.TP
+.BI gamma-day\fR\ =\ 0.1-10.0
+.TQ
+.BI gamma-day\fR\ =\ red : green : blue
+Set gamma correction for daytime. Values must be within [0.1,
+10.0], and are applied to each colour channel individually,
+however if only one value is specified it is applied to all
+each channels.
+.TP
+.BI gamma-night\fR\ =\ 0.1-10.0
+.TQ
+.BI gamma-night\fR\ =\ red : green : blue
+Set gamma correction for nighttime. Values must be within [0.1,
+10.0], and are applied to each colour channel individually,
+however if only one value is specified it is applied to all
+each channels.
+.TP
+.BI hook\fR\ =\ "file or directory"
+Set hook file or directory. If not specified, the default
+paths are searched.
+
+.B /dev/null
+and
+.B /var/empty
+can be used to prevent redshift from executing hooks.
+.TP
+.BI fade\fR\ =\ \fP0 " or " 1
+Disable (if
+.BR 0 )
+or enable (if
+.BR 1 )
+fading between colour settings with large differences.
+
+The
+.B -r
+and
+.B +r
+options can be used to override this setting.
+.TP
+.BI preserve-gamma\fR\ =\ \fP0 " or " 1
+If
+.BR 1 ,
+preapplied colour calibrations (all applied effects are
+assumed to be colour calibrations) will be preserved, if
+.BR 0 ,
+colour calibrations will be reset while
+.B redshift
+is running.
+
+The
+.B -P
+and
+.B +P
+options can be used to override this setting.
+
+Note that if
+.BR 0 ,
+colour calibrations will be reset even when
+.B redshift
+is running but is disabled. This is necessary to
+support the
+.B -o
+and
+.B -O
+options.
+
+This setting is ignored when
+.B coopgamma
+is used as
+.B coopgamma
+allows multiple programs to modify the gamma ramps
+at the same time.
+.TP
+.BI start-disabled\fR\ =\ \fP0 " or " 1
+Start
+.B redshift
+in disabled (if
+.BR 1 )
+or enabled (if
+.BR 0 )
+state.
+
+The
+.B -D
+and
+.B +D
+options can be used to override this setting.
+.TP
+.BI elevation-high\fR\ =\ decimal
+The lowest solar elevation, in degrees, during daytime.
+.TP
+.BI elevation-low\fR\ =\ decimal
+The highest solar elevation, in degrees, during nighttime.
+.TP
+.BI dawn-time\fR\ =\ HH : MM\fR[ : SS\fR][ - HH : MM\fR[ : SS\fR]]
+.TQ
+.BI dusk-time\fR\ =\ HH : MM\fR[ : SS\fR][ - HH : MM\fR[ : SS\fR]]
+Custom time interval for the transition from night to day
+.RB ( dawn-time )
+and for the transition from day to night
+.BR ( dusk-time ).
+
+When specified, both settings must be specified and the
+solar elevation will not be used to determine the current
+daytime/nighttime period, nor will a location provider
+used.
+
+The left-hand hour must be within [0, 23], but the right-hand
+hour may be within [0, 47], the timespan must not be greater
+than 24 hours. The minutes and seconds must be within [0, 59],
+and the default value for the seconds is 0.
+.TP
+.BI adjustment-method\fR\ =\ name
+Select adjustment method. Options for the adjustment method can
+be given under the configuration file heading of the same name.
+
+Not used if the
+.B -p
+option is specified.
+.TP
+.BI location-provider\fR\ =\ name
+Select location provider. Options for the location provider can
+be given under the configuration file heading of the same name.
+
+Not used if
+.B dawn-time
+and
+.B dusk-time
+are used or if the colours
+settings are specified to be the same during the day and the
+night.
+.PP
+Options for the location provider
+.B manual
+are:
+.TP
+.BI lat\fR\ =\ decimal
+The GPS latitude of the user's geographical location.
+Shall be specified in degrees and formatted a single
+real number, rather than split into integer degrees,
+minutes and seconds. Positive values used for the
+northern hemisphere and negative values are ued for
+the southern hemisphere.
+.TP
+.BI lon\fR\ =\ decimal
+The GPS longitude of the user's geographical location.
+Shall be specified in degrees and formatted a single
+real number, rather than split into integer degrees,
+minutes and seconds. Positive values used for the
+eastern hemisphere and negative values are ued for
+the western hemisphere.
+.PP
+There are no options for the location providers
+.B geoclue2
+(may be available on Unix-like systems) and
+.B corelocation
+(available on Mac OS X).
+.PP
+Options for the adjustment method
+.B randr
+(preferred method for X) are:
+.TP
+.BI display\fR\ =\ name
+X display to apply adjustments to. Default is determined
+by the environment variable
+.IR DISPLAY .
+
+The value is expected to contain a colon
+.RB ( : )
+and can only be terminated with a semicolon
+.RB ( ; ).
+.TP
+.BI screen\fR\ =\ "ordinal list or " all
+Comma-separated
+.RB ( , )
+list of X screens to apply adjustments to.
+All available X screens are used if the list is empty or
+if the setting is omitted.
+
+.B all
+may be specified as a synonym for an empty list.
+.TP
+.BI crtc\fR\ =\ "number list or " all
+Comma-separated
+.RB ( , )
+list of CRTC numbers for monitors to
+apply adjustments to. All available CRTCs are used if the
+list is empty or if the setting is omitted.
+
+A CRTC number may either be specified as an overall
+ordinal for all selected X screens, or the index of
+the X screen followed by a dot
+.RB ( . )
+and the index of the CRTC within the specified X screen.
+The specified X screen is automatically included in the
+.B screen
+selection.
+
+.B all
+may be specified as a synonym for an empty list.
+
+The index of the first CRTC is 0.
+.TP
+.BI edid\fR\ =\ "name list or " list
+Comma-separated
+.RB ( , )
+list of EDIDs of monitors to apply adjustments to.
+
+If
+.B list
+is specified, all available EDIDs will be
+printed to the standard output and the program exits.
+
+This list must not be empty; to select all monitors,
+instead specify
+.BR crtc=all .
+.PP
+Options for the adjustment method
+.B vidmode
+(fallback method for X) are:
+.TP
+.BI display\fR\ =\ name
+X display to apply adjustments to. Default is determined
+by the environment variable
+.IR DISPLAY .
+
+The value is expected to contain a colon
+.RB ( : )
+and can only be terminated with a semicolon
+.RB ( ; ).
+.TP
+.BI screen\fR\ =\ "ordinal list or " all
+Comma-separated
+.RB ( , )
+list of X screens to apply adjustments to.
+All available X screens are used if the list is empty or
+if the setting is omitted.
+
+.B all
+may be specified as a synonym for an empty list.
+.PP
+Options for the adjustment method
+.B drm
+(method for Linux without display server) are:
+.TP
+.BI card\fR\ =\ "ordinal list or " all
+Comma-separated
+.RB ( , )
+list of indices of graphics card screens to apply
+adjustments to. All available graphics cards
+are used if the list is empty or
+if the setting is omitted.
+
+.B all
+may be specified as a synonym for an empty list.
+.TP
+.BI crtc\fR\ =\ "number list or " all
+Comma-separated
+.RB ( , )
+list of CRTC numbers for monitors to
+apply adjustments to. All available CRTCs are used if the
+list is empty or if the setting is omitted.
+
+A CRTC number may either be specified as an overall
+ordinal for all selected graphics card, or the index of
+the graphics card followed by a dot
+.RB ( . )
+and the index of
+the CRTC within the specified graphics card. The specified
+graphics card is automatically included in the
+.B card
+selection.
+
+.B all
+may be specified as a synonym for an empty list.
+
+The index of the first CRTC is 0.
+.TP
+.BI edid\fR\ =\ "name list or " list
+Comma-separated
+.RB ( , )
+list of EDIDs of monitors to apply adjustments to.
+
+If
+.B list
+is specified, all available EDIDs will be
+printed to the standard output and the program exits.
+
+This list must not be empty; to select all monitors,
+instead specify
+.BR crtc=all .
+.PP
+There are no options for the adjustment methods
+.B wingdi
+(available on Windows),
+.B quartz
+(available on Mac OS X), and
+.B dummy
+(used for debugging, does not apply any colour effects).
+
+.SS Hooks
+Executable files (that are not dotfiles or tilde files) in the hook
+directory (see the
+.B FILES
+section), are executed on certain events.
+The file inherit the
+.B redshift
+process standard input and standard
+error, however the standard output is redirected to the standard error.
+.PP
+Each file is executed with at least one argument. This first argument
+indicate what event has taked place. Additional arguments may be
+provided for additional event data.
+.PP
+.B redshift
+will be the parent process of the executed script.
+.PP
+Currently available events are:
+.TP
+.B period-changed
+This indicate that the period of the day has changed (and at
+start of continual mode). The script will be provided two
+additional arguments (the second argument and the third
+argument). The second argument will the previous period, and
+the third argument will be the new period. The argument values
+will be either of
+.BR night ,
+.BR daytime ,
+.BR transition
+(transition in either direction between night and daytime), or
+.B none
+(not previously or no longer calculated).
+.B none
+appears as the
+previous period at start up and can also appear when the when
+.B redshift
+is reconfigured if the colour settings no longer
+depending on the period of the day or has started depending
+on the period of the day.
+
+.SS Gamma ramps
+.B redshift
+applies a redness effect to the graphical display. The
+intensity of the redness can be customised and scheduled to only be
+applied at night or to be applied with more intensity at night.
+.PP
+.B redshift
+uses colour correction lookup tables (CLUTs), usually called
+gamma ramps or gamma correction ramps, to apply this effect.
+
+.SS Colour temperature
+The redness effect applied by
+.B redshift is modelled after black-body
+radiation, specifically with a 10 degree observer. Although black-body
+radiation starts at 0,
+.BR redshift 's
+model start at the conventional 1000K
+(1000 Kelvin). For this reason, no colour temperature below 1000K can be
+specified. However, as there is a limit can be determined for the colour
+when the colour temperature appreciates infinity, the upper limit for
+allow colour temperature is instead determined by the data type it is
+stored in. However, it also means that it is meaningless to use colour
+temperatures above 40000K.
+.PP
+The sRGB colour space, and modern monitors, use the standard illuminant
+D65 as the reference for pure white, modelling ideal day light. The
+correlated colour temperature of D65 is called 6500K, however it's
+actually 6504K, but
+.BR redshift 's
+defines this illuminant has having the colour temperature 6500K.
+This means that 6500K is the neutral (no effect) colour temperature.
+.PP
+The current version
+.B redshift
+assumes the monitor uses sRGB. However
+this is usually only true for CRT monitors. HDR-capable monitors
+particular diverges significant for sRGB. This means that the display
+colour does not perfectly correlated to the specified colour temperate.
+Lower (more red) colour temperatures, about 1900K and below, are out of
+gamut, and thus incorrect even on sRGB monitors.
+
+.SH EXIT STATUS
+Default.
+
+.SH EXAMPLES
+Example for the superelliptical roundabout in Stockholm, Sweden:
+.RS
+.nf
+
+redshift -l 59.333:18.065 -t 5700:3600 -b 1:0.8
+.fi
+.RE
+.PP
+Example configuration file equivalent to above command:
+.RS
+.nf
+
+[redshift]
+temperature-day=5700
+temperature-night=3600
+brightness-day=1
+brightness-night=0.8
+location-provider=manual
+
+[manual]
+lat=59.333
+lon=18.065
+.fi
+.RE
+.PP
+Sample hook script:
+.RS
+.nf
+
+#!/bin/sh
+case "$1" in
+ period-changed)
+ notify-send "redshift-ng" "Period changed from $2 to $3";;
+esac
+.fi
+.RE
+
+.SH KNOWN ISSUES
+.SS No or incorrect effect on cursor
+Some graphics drivers apply the effect (colour corrects) twice or not at
+all on hardware cursors. It is often possible to reconfigure the display
+server to use software cursors, to avoid this problem, however at mouse
+pointer performance cost that may be noticeable on very low-end computer.
+
+.SS D65-flashes
+For some versions of some graphics drivers, there will be an occasional
+flash where gamma ramps are not applied to the output.
+
+.SS Limited hardware support
+Low-end hardware, especially embedded devices, often lack colour
+correction features
+.B redshift
+abuse to apply its effect.
+.B redshift
+is not always able to tell if support is missing.
+
+.SS Limited software support
+.B redshift
+does not yet support Wayland. If your environment contains the
+variable
+.IR WAYLAND_DISPLAY ,
+you are using a Wayland compositor and cannot
+currently expect
+.B redshift
+to work. Even with Wayland support, it would be up to each individual
+Wayland compositor to opt in to support applications like
+.BR redshift .
+
+.SS Backlight control
+.B redshift
+uses gamma ramps rather than backlight control to adjust
+brightness. This actually intentional and for your best. Most
+contemporary monitors require Pulse-Width Modulation, which causes
+flicker than can cause eye-strain and headaches, to adjust backlight.
+Using gamma ramps is a safe option, it's also considerably less work
+basically no extra code and posses no additional limitations. It's often
+not possible to adjust backlight on desktop monitors from software, for
+devices for which it is possible (mostly telephones and laptops, however
+not all have fine-grained enough configurability to be usable) it's not
+possible from software to determine well enough how changing the
+backlight settings changes the backlight physically. If you still want
+backlight to be controlled, you can hook in a tool such as
+.BR adjbacklight (1).
+
+.SS Flickering and temporary suspension
+.B redshift
+uses the gamma ramps for the monitor to apply it's effect. The
+gamma ramps where originally intended for colour correction. Therefore
+there is no standardised why have multiple applications applying
+different effects without overriding each other. This can cause
+continuous flicker if multiple instance are running or effects
+temporarily disappearing. By default,
+.B redshift
+uses
+.BR coopgammad (1),
+which is a daemon applications can opt to use instead of directly
+setting the gamma ramps themselves,
+.BR coopgammad (1)
+can then calculate the result of all
+of the effects and apply them as one, allowing the user to use multiple
+applications that apply different effects. However
+.BR coopgammad (1)
+still has to compete with applications that does not use it.
+
+.SS DRM and display servers
+Using the DRM gamma ramp adjustment method can block starting or
+switching to and already started display server (like X). Users may
+also find that trying to switch to and an already started display cases
+the computer hang, or more precisely appear to hang, as the display
+server is not beign presented, the screen freezes, and the keyboard
+doesn't do anything. (Once upon a time, this wasn't as catastrophic,
+and it probably depend on display server implementation details.) The
+only solution, abort from restarting the computer, is to remote into
+it and kill the display server.
+
+.SH RATIONALE
+To prevent the user from accidental making the screen black, brightness
+level below 0.1 are forbidden.
+.PP
+To prevent colour distortion and making the screen too white, brightness
+level above 1.0 are forbidden.
+.PP
+Gamma correction is preserved for backwards compatibility and is
+deprecated (gamma parameters in particular).
+.PP
+.RB \(dq : \(dq
+was used as the option delimited for
+.B -l
+and
+.B -m
+in the original
+.BR redshift ,
+this is preserved for backwards compatbility. However because
+some new options are expected to have
+.RB \(dq : \(dq
+in their value,
+.RB \(dq ; \(dq
+has added as an additional delimiter. Despite this
+.RB \(dq : \(dq
+is still the preferred delimiter as it is more user-friendly
+and use of options that require delimiting with
+.RB \(dq ; \(dq
+is uncommon.
+
+.SH NOTES
+\(dqColour temperature\(dq, or just \(dqtemperature\(dq, is actually short for
+\(dqcorrelated colour temperature\(dq. (Your monitor is not a black-body
+radiator.) And specifically the correlated colour temperature of the
+monitor's whitepoint.
+.PP
+It's common for users to miss to specify a coordinate as negative,
+which, if missed on the longitude can swap day and night. The latitude
+is negative on the southern hemisphere and the longitude is negative on
+the western hemisphere.
+
+.SH SEE ALSO
+.BR cg-tools (7),
+.BR coopgammad (1),
+.BR radharc (1)
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;
+}
diff --git a/redshift/signals.c b/redshift/signals.c
new file mode 100644
index 0000000..cd23773
--- /dev/null
+++ b/redshift/signals.c
@@ -0,0 +1,209 @@
+/*-
+ * 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"
+
+
+volatile sig_atomic_t exiting = 0;
+volatile sig_atomic_t disable = 0;
+volatile enum signals signals = 0;
+
+
+/**
+ * Signal handler for exit signals (SIGINT, SIGTERM, SIGQUIT)
+ *
+ * @param signo The received signal
+ */
+static void
+sigexit(int signo)
+{
+ exiting = 1;
+#ifdef WINDOWS
+ signal(signo, &sigexit);
+#endif
+ (void) signo;
+}
+
+
+#ifndef WINDOWS
+
+/**
+ * Signal handler for disable signal (SIGUSR1)
+ *
+ * @param signo The received signal
+ */
+static void
+sigdisable(int signo)
+{
+ disable = 1;
+ (void) signo;
+}
+
+
+/**
+ * Signal handler for forceful exiting; installed by
+ * `install_forceful_exit_signal_handlers`
+ *
+ * @param signo The received signal
+ */
+static void
+sigalrm(int signo)
+{
+ if (exiting || signo == SIGALRM)
+ exit(0);
+ exiting = 1;
+ alarm(1U);
+}
+
+
+/**
+ * Signal handler for SIGUSR2
+ *
+ * @param signo The received signal
+ * @param info The received signal data
+ * @param uctx Interrupted stack context
+ */
+static void
+sigipc(int signo, siginfo_t *info, void *uctx)
+{
+ int set, mask;
+ sigset_t sigusr2_mask;
+
+ (void) signo;
+ (void) uctx;
+
+ if (info->si_code != SI_QUEUE)
+ return;
+
+ switch (info->si_value.sival_int) {
+ case 1:
+ case 2:
+ mask = 3 << 1;
+ break;
+
+ case 5:
+ case 6:
+ mask = 3 << 5;
+ break;
+
+ case 7:
+ case 8:
+ mask = 3 << 7;
+ break;
+
+ case 11:
+ case 12:
+ mask = 3 << 11;
+ break;
+
+ case 0:
+ case 3:
+ case 4:
+ case 9:
+ case 10:
+ mask = 0;
+ break;
+
+ default:
+ return;
+ }
+
+ signals |= set = 1 << info->si_value.sival_int;
+ signals &= ~mask | set;
+
+ if (set == SIGNAL_ORDER_BARRIER) {
+ sigemptyset(&sigusr2_mask);
+ sigaddset(&sigusr2_mask, SIGUSR2);
+ if (sigprocmask(SIG_BLOCK, &sigusr2_mask, NULL))
+ eprintf("sigprocmask:");
+ }
+}
+
+#endif
+
+
+void
+install_signal_handlers(void)
+{
+#ifdef WINDOWS
+ if (signal(SIGINT, &sigexit) == SIG_ERR)
+ eprintf("signal SIGINT <function pointer>:");
+ if (signal(SIGTERM, &sigexit) == SIG_ERR)
+ eprintf("signal SIGTERM <function pointer>:");
+
+#else
+ struct sigaction sigact;
+ sigset_t sigset;
+
+ memset(&sigact, 0, sizeof(sigact));
+ sigemptyset(&sigset);
+ sigact.sa_mask = sigset;
+
+ sigact.sa_flags = SA_NODEFER;
+
+ sigact.sa_handler = &sigexit;
+ if (sigaction(SIGINT, &sigact, NULL))
+ eprintf("sigaction SIGINT &{.sa_handler=<function pointer>, .sa_mask={}, .sa_flags=SA_NODEFER} NULL:");
+ if (sigaction(SIGTERM, &sigact, NULL))
+ eprintf("sigaction SIGTERM &{.sa_handler=<function pointer>, .sa_mask={}, .sa_flags=SA_NODEFER} NULL:");
+ if (sigaction(SIGQUIT, &sigact, NULL))
+ eprintf("sigaction SIGQUIT &{.sa_handler=<function pointer>, .sa_mask={}, .sa_flags=SA_NODEFER} NULL:");
+
+ sigact.sa_handler = &sigdisable;
+ if (sigaction(SIGUSR1, &sigact, NULL))
+ eprintf("sigaction SIGUSR1 &{.sa_handler=<function pointer>, .sa_mask={}, .sa_flags=SA_NODEFER} NULL:");
+
+ sigact.sa_flags = 0;
+
+ sigact.sa_handler = SIG_IGN; /* cause child processes (hooks) to be reaped automatically */
+ if (sigaction(SIGCHLD, &sigact, NULL))
+ eprintf("sigaction SIGCHLD &{.sa_handler=SIG_IGN, .sa_mask={}, .sa_flags=0} NULL:");
+
+ sigact.sa_flags = SA_SIGINFO;
+
+ sigact.sa_sigaction = &sigipc;
+ if (sigaction(SIGUSR2, &sigact, NULL))
+ eprintf("sigaction SIGUSR2 &{.sa_sigaction=<function pointer>, .sa_mask={}, .sa_flags=SA_SIGINFO} NULL:");
+#endif
+}
+
+
+#ifndef WINDOWS
+void
+install_forceful_exit_signal_handlers(void)
+{
+ struct sigaction sigact;
+ sigset_t sigset;
+
+ exiting = 0;
+ memset(&sigact, 0, sizeof(sigact));
+ sigemptyset(&sigset);
+ sigact.sa_mask = sigset;
+ sigact.sa_flags = 0;
+ sigact.sa_handler = &sigalrm;
+ if (sigaction(SIGINT, &sigact, NULL))
+ eprintf("sigaction SIGINT &{.sa_handler=<function pointer>, .sa_mask={}, .sa_flags=0} NULL:");
+ if (sigaction(SIGTERM, &sigact, NULL))
+ eprintf("sigaction SIGTERM &{.sa_handler=<function pointer>, .sa_mask={}, .sa_flags=0} NULL:");
+ if (sigaction(SIGQUIT, &sigact, NULL))
+ eprintf("sigaction SIGQUIT &{.sa_handler=<function pointer>, .sa_mask={}, .sa_flags=0} NULL:");
+ if (sigaction(SIGALRM, &sigact, NULL))
+ eprintf("sigaction SIGALRM &{.sa_handler=<function pointer>, .sa_mask={}, .sa_flags=0} NULL:");
+}
+#endif
diff --git a/redshift/util.c b/redshift/util.c
new file mode 100644
index 0000000..26e09a7
--- /dev/null
+++ b/redshift/util.c
@@ -0,0 +1,317 @@
+/*-
+ * 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"
+
+
+char *
+rtrim(char *s, char *end)
+{
+ end = end ? end : strchr(s, '\0');
+ while (end != s && (end[-1] == ' ' || end[-1] == '\t'))
+ end--;
+ *end = '\0';
+ return s;
+}
+
+
+char *
+ltrim(char *s)
+{
+ while (*s == ' ' || *s == '\t')
+ s++;
+ return s;
+}
+
+
+const char *
+get_home(void)
+{
+#ifdef WINDOWS
+ return NULL;
+#else
+ static const char *home = NULL;
+ struct passwd *pw;
+ if (!home) {
+ pw = getpwuid(getuid());
+ if (pw) {
+ home = pw->pw_dir;
+ if (home && *home)
+ return home;
+ weprintf(_("Cannot determine your home directory, "
+ "it is from the system's user table."));
+ } else if (errno) {
+ weprintf("getpwuid:");
+ } else {
+ weprintf(_("Cannot determine your home directory, your"
+ " user ID is missing from the system's user table."));
+ /* `errno` can either be set to any number of error codes,
+ * or be zero if the user does not have a passwd entry */
+ }
+ home = "";
+ }
+ return home;
+#endif
+}
+
+
+/**
+ * Search for a file and open it in some manner
+ *
+ * @param path_spec Specification for the path to try
+ * @param path_out Output parameter for the found file
+ * @param pathbuf_out Output parameter for the memory allocation for `*path_out`;
+ * shall be free(3)d by the caller
+ * @param open_cb Pointer to function used to open the file, unless it
+ * returns `NULL` it's return value is returned by the
+ * function (`try_path`); `NULL` shall be returned if the
+ * file `path` does not exist
+ * @return File access object to the found file; `NULL` if not found
+ */
+static void *
+try_path(const struct env_path *path_spec, const char **path_out, char **pathbuf_out, void *(*open_cb)(const char *path))
+{
+ const char *prefix, *p, *q;
+ char *path;
+ size_t len;
+ void *f = NULL;
+
+ *path_out = NULL;
+ *pathbuf_out = NULL;
+
+ if (!path_spec->prefix_env) {
+ prefix = get_home();
+ } else if (*path_spec->prefix_env) {
+ prefix = getenv(path_spec->prefix_env);
+ } else {
+ f = (*open_cb)(path_spec->suffix);
+ if (f)
+ *path_out = path_spec->suffix;
+ return f;
+ }
+ if (!prefix || !*prefix)
+ return NULL;
+
+ path = emalloc(strlen(prefix) + strlen(path_spec->suffix) + 1U);
+
+ if (path_spec->multidir_env) {
+ for (p = prefix; !f && *p; p = &q[!!*q]) {
+#ifdef strchrnul
+ q = strchrnul(p, PATH_DELIMITER);
+#else
+ q = strchr(p, PATH_DELIMITER);
+ q = q ? q : strchr(p, '\0');
+#endif
+ len = (size_t)(q - p);
+ if (!len)
+ continue;
+
+ memcpy(path, p, len);
+ stpcpy(&path[len], path_spec->suffix);
+ f = (*open_cb)(path);
+ }
+ } else {
+ stpcpy(stpcpy(path, prefix), path_spec->suffix);
+ f = (*open_cb)(path);
+ }
+
+ if (f)
+ *path_out = *pathbuf_out = path;
+ else
+ free(path);
+ return f;
+}
+
+
+/**
+ * Open a file for reading, if it exists
+ *
+ * @param path The path to the file
+ * @return `FILE` object for reading the file,
+ * `NULL` if it doesn't exist
+ */
+static void *
+open_file(const char *path)
+{
+ FILE *f = fopen(path, "r");
+ if (!f && errno != ENOENT)
+ eprintf("fopen %s \"r\":", path);
+ return f;
+}
+
+
+FILE *
+try_path_fopen(const struct env_path *path_spec, const char **path_out, char **pathbuf_out)
+{
+ return try_path(path_spec, path_out, pathbuf_out, &open_file);
+}
+
+
+/**
+ * Open a directory for reading, if it exists
+ *
+ * @param path The path to the directory
+ * @return `DIR` object for reading the directory,
+ * `NULL` if it doesn't exist
+ */
+static void *
+open_dir(const char *path)
+{
+ DIR *f = opendir(path);
+ if (!f && errno != ENOENT)
+ eprintf("opendir %s:", path);
+ return f;
+}
+
+
+DIR *
+try_path_opendir(const struct env_path *path_spec, const char **path_out, char **pathbuf_out)
+{
+ return try_path(path_spec, path_out, pathbuf_out, &open_dir);
+}
+
+
+
+#ifndef WINDOWS
+void
+pipe_rdnonblock(int pipefds[2])
+{
+ int i, flags;
+
+ /* Try to use pipe2(2) create O_CLOEXEC pipe */
+# if defined(__linux__) && !defined(MISSING_PIPE2)
+ if (!pipe2(pipefds, O_CLOEXEC))
+ goto apply_nonblock;
+ else if (errno != ENOSYS)
+ eprintf("pipe2 <buffer> O_CLOEXEC:");
+# endif
+
+ /* Fallback for when pipe2(2) is not available */
+ if (pipe(pipefds))
+ eprintf("pipe:");
+ for (i = 0; i < 2; i++) {
+ flags = fcntl(pipefds[i], F_GETFD);
+ if (flags == -1)
+ eprintf("fcntl <pipe> F_GETFD:");
+ if (fcntl(pipefds[i], F_SETFD, flags | O_CLOEXEC))
+ eprintf("fcntl <pipe> F_SETFD +O_CLOEXEC:");
+ }
+
+ /* Make the read-end non-blocking */
+# if defined(__linux__) && !defined(MISSING_PIPE2)
+apply_nonblock:
+# endif
+ flags = fcntl(pipefds[0], F_GETFL);
+ if (flags == -1)
+ eprintf("fcntl <pipe> F_GETFL:");
+ if (fcntl(pipefds[0], F_SETFL, flags | O_NONBLOCK))
+ eprintf("fcntl <pipe> F_SETFL +O_NONBLOCK:");
+}
+#endif
+
+
+void *
+ecalloc(size_t n, size_t m)
+{
+ char *ret = calloc(n, m);
+ if (!ret)
+ eprintf("calloc:");
+ return ret;
+}
+
+
+void *
+emalloc(size_t n)
+{
+ char *ret = malloc(n);
+ if (!ret)
+ eprintf("malloc:");
+ return ret;
+}
+
+
+void *
+erealloc(void *ptr, size_t n)
+{
+ char *ret = realloc(ptr, n);
+ if (!ret)
+ eprintf("realloc:");
+ return ret;
+}
+
+
+char *
+estrdup(const char *s)
+{
+ char *ret = strdup(s);
+ if (!ret)
+ eprintf("strdup:");
+ return ret;
+}
+
+
+void
+vweprintf(const char *fmt, va_list args)
+{
+ int saved_errno;
+ const char *errstrprefix, *errstr;
+
+ saved_errno = errno;
+ if (!*fmt) {
+ errstrprefix = "";
+ errstr = strerror(saved_errno);
+ } else if (strchr(fmt, '\0')[-1] == '\n') {
+ errstrprefix = "";
+ errstr = NULL;
+ } else if (strchr(fmt, '\0')[-1] == ':') {
+ errstrprefix = " ";
+ errstr = strerror(saved_errno);
+ } else {
+ errstrprefix = "";
+ errstr = "";
+ }
+
+ fprintf(stderr, "%s: ", argv0);
+ vfprintf(stderr, fmt, args);
+ if (errstr)
+ fprintf(stderr, "%s%s\n", errstrprefix, errstr);
+
+ errno = saved_errno;
+}
+
+
+void
+weprintf(const char *fmt, ...)
+{
+ va_list args;
+ va_start(args, fmt);
+ vweprintf(fmt, args);
+ va_end(args);
+}
+
+
+void
+eprintf(const char *fmt, ...)
+{
+ va_list args;
+ va_start(args, fmt);
+ vweprintf(fmt, args);
+ va_end(args);
+ exit(1);
+}
diff --git a/redshift/windows/appicon.rc b/redshift/windows/appicon.rc
new file mode 100644
index 0000000..9980b7e
--- /dev/null
+++ b/redshift/windows/appicon.rc
@@ -0,0 +1 @@
+AppIcon ICON redshift.ico
diff --git a/redshift/windows/redshift.ico b/redshift/windows/redshift.ico
new file mode 100644
index 0000000..751e6fa
--- /dev/null
+++ b/redshift/windows/redshift.ico
Binary files differ
diff --git a/redshift/windows/versioninfo.rc b/redshift/windows/versioninfo.rc
new file mode 100644
index 0000000..9ede49d
--- /dev/null
+++ b/redshift/windows/versioninfo.rc
@@ -0,0 +1,20 @@
+#include "config.h"
+
+1 VERSIONINFO
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "040904E4"
+ BEGIN
+ VALUE "CompanyName", "Redshift Open Source Project"
+ VALUE "FileDescription", "Redshift"
+ VALUE "OriginalFilename", "redshift.exe"
+ VALUE "ProductName", "Redshift"
+ VALUE "ProductVersion", PACKAGE_VERSION
+ END
+ END
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x409, 1252
+ END
+END