aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.builds/alpine.yml13
-rw-r--r--.builds/archlinux.yml12
-rw-r--r--.builds/freebsd.yml12
-rw-r--r--.editorconfig8
-rw-r--r--.gitignore2
-rw-r--r--LICENSE7
-rw-r--r--README.md27
-rw-r--r--color.c152
-rw-r--r--color.h16
-rw-r--r--main.c447
-rw-r--r--meson.build79
-rw-r--r--meson_options.txt1
-rw-r--r--str_vec.c35
-rw-r--r--str_vec.h15
-rw-r--r--wlr-gamma-control-unstable-v1.xml126
-rw-r--r--wsct.1.scd41
16 files changed, 993 insertions, 0 deletions
diff --git a/.builds/alpine.yml b/.builds/alpine.yml
new file mode 100644
index 0000000..ea4ced2
--- /dev/null
+++ b/.builds/alpine.yml
@@ -0,0 +1,13 @@
+image: alpine/edge
+packages:
+ - meson
+ - wayland
+ - wayland-dev
+ - wayland-protocols
+sources:
+ - https://git.sr.ht/~kennylevinsen/wlsunset
+tasks:
+ - build: |
+ meson build wlsunset
+ ninja -C build
+
diff --git a/.builds/archlinux.yml b/.builds/archlinux.yml
new file mode 100644
index 0000000..3882ea6
--- /dev/null
+++ b/.builds/archlinux.yml
@@ -0,0 +1,12 @@
+image: archlinux
+packages:
+ - meson
+ - wayland
+ - wayland-protocols
+sources:
+ - https://git.sr.ht/~kennylevinsen/wlsunset
+tasks:
+ - build: |
+ meson build wlsunset
+ ninja -C build
+
diff --git a/.builds/freebsd.yml b/.builds/freebsd.yml
new file mode 100644
index 0000000..ac8481e
--- /dev/null
+++ b/.builds/freebsd.yml
@@ -0,0 +1,12 @@
+image: freebsd/latest
+packages:
+ - meson
+ - wayland
+ - wayland-protocols
+ - pkgconf
+sources:
+ - https://git.sr.ht/~kennylevinsen/wlsunset
+tasks:
+ - build: |
+ meson build wlsunset
+ ninja -C build
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..51a7d86
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,8 @@
+root = true
+
+[*]
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+indent_style = tab
+indent_size = 8
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a4fb4fb
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+build/
+.cache/
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..c81977f
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,7 @@
+Copyright 2020 Kenny Levinsen
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..20b9131
--- /dev/null
+++ b/README.md
@@ -0,0 +1,27 @@
+# wsct
+
+Set a fixed color temperature on Wayland compositors supporting
+`wlr-gamma-control-unstable-v1`.
+Stripped down version of <https://git.sr.ht/~kennylevinsen/wlsunset> with only the funcionality I need.
+
+# How to build and install
+
+```sh
+meson build
+ninja -C build
+sudo ninja -C build install
+```
+
+# How to use
+
+```sh
+wsct 4500
+```
+
+`wsct` stays running after applying the temperature. The Wayland gamma-control
+protocol restores the original ramps when the client exits, so the process must
+remain alive for the setting to persist.
+
+# Help
+
+Go to #kennylevinsen @ irc.libera.chat to discuss, or use [~kennylevinsen/wlsunset-devel@lists.sr.ht](https://lists.sr.ht/~kennylevinsen/wlsunset-devel)
diff --git a/color.c b/color.c
new file mode 100644
index 0000000..217fcb8
--- /dev/null
+++ b/color.c
@@ -0,0 +1,152 @@
+#define _USE_MATH_DEFINES
+#define _XOPEN_SOURCE 700
+#define _POSIX_C_SOURCE 200809L
+#include <errno.h>
+#include <math.h>
+
+#include "color.h"
+
+/*
+ * Illuminant D, or daylight locus, is is a "standard illuminant" used to
+ * describe natural daylight as we perceive it, and as such is how we expect
+ * bright, cold white light sources to look. This is different from the
+ * planckian locus due to the effects of the atmosphere on sunlight travelling
+ * through it.
+ *
+ * It is on this locus that D65, the whitepoint used by most monitors and
+ * assumed by display servers, is defined.
+ *
+ * This approximation is strictly speaking only well-defined between 4000K and
+ * 25000K, but we stretch it a bit further down for transition purposes.
+ */
+static int illuminant_d(int temp, double *x, double *y) {
+ // https://en.wikipedia.org/wiki/Standard_illuminant#Illuminant_series_D
+ if (temp >= 2500 && temp <= 7000) {
+ *x = 0.244063 +
+ 0.09911e3 / temp +
+ 2.9678e6 / pow(temp, 2) -
+ 4.6070e9 / pow(temp, 3);
+ } else if (temp > 7000 && temp <= 25000) {
+ *x = 0.237040 +
+ 0.24748e3 / temp +
+ 1.9018e6 / pow(temp, 2) -
+ 2.0064e9 / pow(temp, 3);
+ } else {
+ errno = EINVAL;
+ return -1;
+ }
+ *y = (-3 * pow(*x, 2)) + (2.870 * (*x)) - 0.275;
+ return 0;
+}
+
+/*
+ * Planckian locus, or black body locus, describes the color of a black body at
+ * a certain temperatures directly at its source, rather than observed through
+ * a thick atmosphere.
+ *
+ * While we are used to bright light coming from afar and going through the
+ * atmosphere, we are used to seeing dim incandescent light sources from close
+ * enough for the atmosphere to not affect its perception, dictating how we
+ * expect dim, warm light sources to look.
+ *
+ * This approximation is only valid from 1667K to 25000K.
+ */
+static int planckian_locus(int temp, double *x, double *y) {
+ // https://en.wikipedia.org/wiki/Planckian_locus#Approximation
+ if (temp >= 1667 && temp <= 4000) {
+ *x = -0.2661239e9 / pow(temp, 3) -
+ 0.2343589e6 / pow(temp, 2) +
+ 0.8776956e3 / temp +
+ 0.179910;
+ if (temp <= 2222) {
+ *y = -1.1064814 * pow(*x, 3) -
+ 1.34811020 * pow(*x, 2) +
+ 2.18555832 * (*x) -
+ 0.20219683;
+ } else {
+ *y = -0.9549476 * pow(*x, 3) -
+ 1.37418593 * pow(*x, 2) +
+ 2.09137015 * (*x) -
+ 0.16748867;
+ }
+ } else if (temp > 4000 && temp < 25000) {
+ *x = -3.0258469e9 / pow(temp, 3) +
+ 2.1070379e6 / pow(temp, 2) +
+ 0.2226347e3 / temp +
+ 0.240390;
+ *y = 3.0817580 * pow(*x, 3) -
+ 5.87338670 * pow(*x, 2) +
+ 3.75112997 * (*x) -
+ 0.37001483;
+ } else {
+ errno = EINVAL;
+ return -1;
+ }
+ return 0;
+}
+
+static double clamp(double value) {
+ if (value > 1.0) {
+ return 1.0;
+ } else if (value < 0.0) {
+ return 0.0;
+ } else {
+ return value;
+ }
+}
+
+static struct rgb xyz_to_rgb(const struct xyz *xyz) {
+ // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
+ return (struct rgb) {
+ .r = pow(clamp(3.2404542 * xyz->x - 1.5371385 * xyz->y - 0.4985314 * xyz->z), 1.0 / 2.2),
+ .g = pow(clamp(-0.9692660 * xyz->x + 1.8760108 * xyz->y + 0.0415560 * xyz->z), 1.0 / 2.2),
+ .b = pow(clamp(0.0556434 * xyz->x - 0.2040259 * xyz->y + 1.0572252 * xyz->z), 1.0 / 2.2)
+ };
+}
+
+static void rgb_normalize(struct rgb *rgb) {
+ double maxw = fmax(rgb->r, fmax(rgb->g, rgb->b));
+ rgb->r /= maxw;
+ rgb->g /= maxw;
+ rgb->b /= maxw;
+}
+
+struct rgb calc_whitepoint(int temp) {
+ if (temp == 6500) {
+ return (struct rgb) {.r = 1.0, .g = 1.0, .b = 1.0};
+ }
+
+ // We are not trying to calculate the accurate whitepoint, but rather
+ // an expected observed whitepoint. We generally expect dim and warm
+ // light sources to follow the planckian locus, while we expect bright
+ // and cold light sources to follow the daylight locus. There is no
+ // "correct" way to transition between these two curves, and so the
+ // goal is purely to be subjectively pleasant/non-jarring.
+ //
+ // A smooth transition between the two in the range between 2500K and
+ // 4000K seems to do the trick for now.
+
+ struct xyz wp;
+ if (temp >= 25000) {
+ illuminant_d(25000, &wp.x, &wp.y);
+ } else if (temp >= 4000) {
+ illuminant_d(temp, &wp.x, &wp.y);
+ } else if (temp >= 2500) {
+ double x1, y1, x2, y2;
+ illuminant_d(temp, &x1, &y1);
+ planckian_locus(temp, &x2, &y2);
+
+ double factor = (4000. - temp) / 1500.;
+ double sinefactor = (cos(M_PI*factor) + 1.0) / 2.0;
+ wp.x = x1 * sinefactor + x2 * (1.0 - sinefactor);
+ wp.y = y1 * sinefactor + y2 * (1.0 - sinefactor);
+ } else {
+ planckian_locus(temp >= 1667 ? temp : 1667, &wp.x, &wp.y);
+ }
+ wp.z = 1.0 - wp.x - wp.y;
+
+ struct rgb wp_rgb = xyz_to_rgb(&wp);
+ rgb_normalize(&wp_rgb);
+
+ return wp_rgb;
+}
diff --git a/color.h b/color.h
new file mode 100644
index 0000000..f221298
--- /dev/null
+++ b/color.h
@@ -0,0 +1,16 @@
+#ifndef _COLOR_MATH_H
+#define _COLOR_MATH_H
+
+#include "math.h"
+
+struct rgb {
+ double r, g, b;
+};
+
+struct xyz {
+ double x, y, z;
+};
+
+struct rgb calc_whitepoint(int temp);
+
+#endif
diff --git a/main.c b/main.c
new file mode 100644
index 0000000..7d6d13c
--- /dev/null
+++ b/main.c
@@ -0,0 +1,447 @@
+#define _DEFAULT_SOURCE
+#define _XOPEN_SOURCE 700
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mman.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <wayland-client-protocol.h>
+#include <wayland-client.h>
+
+#include "color.h"
+#include "wlr-gamma-control-unstable-v1-client-protocol.h"
+
+#ifndef WLSUNSET_VERSION
+#define WLSUNSET_VERSION "dev"
+#endif
+
+#define WSCT_GAMMA 1.0
+
+struct app {
+ struct wl_list outputs;
+ struct zwlr_gamma_control_manager_v1 *gamma_control_manager;
+ int temperature;
+};
+
+struct output {
+ struct wl_list link;
+
+ struct app *app;
+ struct wl_output *wl_output;
+ struct zwlr_gamma_control_v1 *gamma_control;
+
+ uint32_t id;
+ uint32_t ramp_size;
+ int table_fd;
+ uint16_t *table;
+ bool applied;
+};
+
+static size_t gamma_table_size(uint32_t ramp_size) {
+ return (size_t)ramp_size * 3 * sizeof(uint16_t);
+}
+
+static void reset_output_table(struct output *output) {
+ if (output->table != NULL) {
+ munmap(output->table, gamma_table_size(output->ramp_size));
+ output->table = NULL;
+ }
+ if (output->table_fd != -1) {
+ close(output->table_fd);
+ output->table_fd = -1;
+ }
+ output->ramp_size = 0;
+}
+
+static void destroy_output(struct output *output) {
+ if (output->gamma_control != NULL) {
+ zwlr_gamma_control_v1_destroy(output->gamma_control);
+ output->gamma_control = NULL;
+ }
+ reset_output_table(output);
+ if (output->wl_output != NULL) {
+ wl_output_destroy(output->wl_output);
+ output->wl_output = NULL;
+ }
+ wl_list_remove(&output->link);
+ free(output);
+}
+
+static int create_anonymous_file(off_t size) {
+ char template[] = "/tmp/wsct-shared-XXXXXX";
+ int fd = mkstemp(template);
+ if (fd < 0) {
+ return -1;
+ }
+
+ if (unlink(template) == -1) {
+ close(fd);
+ return -1;
+ }
+
+ int ret;
+ do {
+ errno = 0;
+ ret = ftruncate(fd, size);
+ } while (ret == -1 && errno == EINTR);
+
+ if (ret == -1) {
+ close(fd);
+ return -1;
+ }
+
+ return fd;
+}
+
+static int create_gamma_table(uint32_t ramp_size, uint16_t **table) {
+ size_t table_size = gamma_table_size(ramp_size);
+ int fd = create_anonymous_file(table_size);
+ if (fd < 0) {
+ perror("failed to create anonymous file");
+ return -1;
+ }
+
+ void *data = mmap(NULL, table_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+ if (data == MAP_FAILED) {
+ perror("failed to mmap gamma table");
+ close(fd);
+ return -1;
+ }
+
+ *table = data;
+ return fd;
+}
+
+static void fill_gamma_table(uint16_t *table, uint32_t ramp_size, double rw,
+ double gw, double bw, double gamma) {
+ uint16_t *r = table;
+ uint16_t *g = table + ramp_size;
+ uint16_t *b = table + 2 * ramp_size;
+
+ for (uint32_t i = 0; i < ramp_size; ++i) {
+ double val = (double)i / (double)(ramp_size - 1);
+ r[i] = (uint16_t)(UINT16_MAX * pow(val * rw, 1.0 / gamma));
+ g[i] = (uint16_t)(UINT16_MAX * pow(val * gw, 1.0 / gamma));
+ b[i] = (uint16_t)(UINT16_MAX * pow(val * bw, 1.0 / gamma));
+ }
+}
+
+static int output_apply_temperature(struct output *output) {
+ if (output->gamma_control == NULL || output->table_fd == -1 || output->table == NULL) {
+ return -1;
+ }
+
+ struct rgb wp = calc_whitepoint(output->app->temperature);
+ fill_gamma_table(output->table, output->ramp_size, wp.r, wp.g, wp.b, WSCT_GAMMA);
+
+ if (lseek(output->table_fd, 0, SEEK_SET) == -1) {
+ perror("failed to rewind gamma table");
+ return -1;
+ }
+
+ zwlr_gamma_control_v1_set_gamma(output->gamma_control, output->table_fd);
+ output->applied = true;
+ fprintf(stderr, "applied %d K to output %u\n", output->app->temperature, output->id);
+ return 0;
+}
+
+static void gamma_control_handle_gamma_size(void *data,
+ struct zwlr_gamma_control_v1 *gamma_control, uint32_t ramp_size) {
+ (void)gamma_control;
+ struct output *output = data;
+
+ reset_output_table(output);
+ if (ramp_size == 0) {
+ fprintf(stderr, "output %u reported a zero-length gamma ramp\n", output->id);
+ return;
+ }
+
+ output->ramp_size = ramp_size;
+ output->table_fd = create_gamma_table(ramp_size, &output->table);
+ if (output->table_fd < 0) {
+ exit(EXIT_FAILURE);
+ }
+
+ if (output_apply_temperature(output) == -1) {
+ exit(EXIT_FAILURE);
+ }
+}
+
+static void gamma_control_handle_failed(void *data,
+ struct zwlr_gamma_control_v1 *gamma_control) {
+ (void)gamma_control;
+ struct output *output = data;
+
+ fprintf(stderr, "gamma control failed for output %u\n", output->id);
+ zwlr_gamma_control_v1_destroy(output->gamma_control);
+ output->gamma_control = NULL;
+ reset_output_table(output);
+ output->applied = false;
+}
+
+static const struct zwlr_gamma_control_v1_listener gamma_control_listener = {
+ .gamma_size = gamma_control_handle_gamma_size,
+ .failed = gamma_control_handle_failed,
+};
+
+static void setup_gamma_control(struct app *app, struct output *output) {
+ if (app->gamma_control_manager == NULL || output->gamma_control != NULL) {
+ return;
+ }
+
+ output->gamma_control = zwlr_gamma_control_manager_v1_get_gamma_control(
+ app->gamma_control_manager, output->wl_output);
+ zwlr_gamma_control_v1_add_listener(output->gamma_control, &gamma_control_listener, output);
+}
+
+static void wl_output_handle_geometry(void *data, struct wl_output *wl_output, int32_t x,
+ int32_t y, int32_t phys_width, int32_t phys_height, int32_t subpixel,
+ const char *make, const char *model, int32_t transform) {
+ (void)data;
+ (void)wl_output;
+ (void)x;
+ (void)y;
+ (void)phys_width;
+ (void)phys_height;
+ (void)subpixel;
+ (void)make;
+ (void)model;
+ (void)transform;
+}
+
+static void wl_output_handle_mode(void *data, struct wl_output *wl_output, uint32_t flags,
+ int32_t width, int32_t height, int32_t refresh) {
+ (void)data;
+ (void)wl_output;
+ (void)flags;
+ (void)width;
+ (void)height;
+ (void)refresh;
+}
+
+static void wl_output_handle_done(void *data, struct wl_output *wl_output) {
+ (void)data;
+ (void)wl_output;
+}
+
+static void wl_output_handle_scale(void *data, struct wl_output *wl_output, int32_t factor) {
+ (void)data;
+ (void)wl_output;
+ (void)factor;
+}
+
+static void wl_output_handle_name(void *data, struct wl_output *wl_output, const char *name) {
+ (void)data;
+ (void)wl_output;
+ (void)name;
+}
+
+static void wl_output_handle_description(void *data, struct wl_output *wl_output,
+ const char *description) {
+ (void)data;
+ (void)wl_output;
+ (void)description;
+}
+
+static const struct wl_output_listener output_listener = {
+ .geometry = wl_output_handle_geometry,
+ .mode = wl_output_handle_mode,
+ .done = wl_output_handle_done,
+ .scale = wl_output_handle_scale,
+ .name = wl_output_handle_name,
+ .description = wl_output_handle_description,
+};
+
+static void registry_handle_global(void *data, struct wl_registry *registry, uint32_t name,
+ const char *interface, uint32_t version) {
+ struct app *app = data;
+ (void)version;
+
+ if (strcmp(interface, wl_output_interface.name) == 0) {
+ struct output *output = calloc(1, sizeof(*output));
+ if (output == NULL) {
+ perror("failed to allocate output");
+ exit(EXIT_FAILURE);
+ }
+
+ output->app = app;
+ output->id = name;
+ output->table_fd = -1;
+ output->wl_output = wl_registry_bind(registry, name, &wl_output_interface, 1);
+ wl_output_add_listener(output->wl_output, &output_listener, output);
+ wl_list_insert(&app->outputs, &output->link);
+
+ setup_gamma_control(app, output);
+ return;
+ }
+
+ if (strcmp(interface, zwlr_gamma_control_manager_v1_interface.name) == 0) {
+ app->gamma_control_manager = wl_registry_bind(registry, name,
+ &zwlr_gamma_control_manager_v1_interface, 1);
+
+ struct output *output;
+ wl_list_for_each(output, &app->outputs, link) {
+ setup_gamma_control(app, output);
+ }
+ }
+}
+
+static void registry_handle_global_remove(void *data, struct wl_registry *registry,
+ uint32_t name) {
+ (void)registry;
+ struct app *app = data;
+ struct output *output, *tmp;
+
+ wl_list_for_each_safe(output, tmp, &app->outputs, link) {
+ if (output->id == name) {
+ destroy_output(output);
+ return;
+ }
+ }
+}
+
+static const struct wl_registry_listener registry_listener = {
+ .global = registry_handle_global,
+ .global_remove = registry_handle_global_remove,
+};
+
+static int parse_temperature(const char *arg, int *temperature) {
+ char *end = NULL;
+ long value;
+
+ errno = 0;
+ value = strtol(arg, &end, 10);
+ if (arg[0] == '\0' || end == arg || *end != '\0') {
+ return -1;
+ }
+ if ((value == LONG_MIN || value == LONG_MAX) && errno == ERANGE) {
+ return -1;
+ }
+ if (value <= 0 || value > INT_MAX) {
+ return -1;
+ }
+
+ *temperature = (int)value;
+ return 0;
+}
+
+static bool any_output_applied(const struct app *app) {
+ const struct output *output;
+ wl_list_for_each(output, &app->outputs, link) {
+ if (output->applied) {
+ return true;
+ }
+ }
+ return false;
+}
+
+static void destroy_outputs(struct app *app) {
+ struct output *output, *tmp;
+ wl_list_for_each_safe(output, tmp, &app->outputs, link) {
+ destroy_output(output);
+ }
+}
+
+static const char usage[] =
+ "usage: %s <temperature>\n"
+ " %s -h\n"
+ " %s -v\n"
+ "\n"
+ "Set the color temperature for all Wayland outputs and keep the process\n"
+ "running so the gamma tables stay active.\n";
+
+int main(int argc, char *argv[]) {
+ if (argc == 2 && strcmp(argv[1], "-h") == 0) {
+ printf(usage, argv[0], argv[0], argv[0]);
+ return EXIT_SUCCESS;
+ }
+
+ if (argc == 2 && strcmp(argv[1], "-v") == 0) {
+ printf("wsct version %s\n", WLSUNSET_VERSION);
+ return EXIT_SUCCESS;
+ }
+
+ if (argc != 2) {
+ fprintf(stderr, usage, argv[0], argv[0], argv[0]);
+ return EXIT_FAILURE;
+ }
+
+ struct app app = { 0 };
+ wl_list_init(&app.outputs);
+
+ if (parse_temperature(argv[1], &app.temperature) == -1) {
+ fprintf(stderr, "invalid temperature: %s\n", argv[1]);
+ return EXIT_FAILURE;
+ }
+
+ struct wl_display *display = wl_display_connect(NULL);
+ if (display == NULL) {
+ fprintf(stderr, "failed to connect to the Wayland display\n");
+ return EXIT_FAILURE;
+ }
+
+ struct wl_registry *registry = wl_display_get_registry(display);
+ wl_registry_add_listener(registry, &registry_listener, &app);
+
+ if (wl_display_roundtrip(display) == -1) {
+ fprintf(stderr, "failed to query the Wayland registry\n");
+ wl_registry_destroy(registry);
+ wl_display_disconnect(display);
+ return EXIT_FAILURE;
+ }
+
+ if (app.gamma_control_manager == NULL) {
+ fprintf(stderr, "compositor doesn't support wlr-gamma-control-unstable-v1\n");
+ wl_registry_destroy(registry);
+ destroy_outputs(&app);
+ wl_display_disconnect(display);
+ return EXIT_FAILURE;
+ }
+
+ if (wl_list_empty(&app.outputs)) {
+ fprintf(stderr, "no Wayland outputs found\n");
+ zwlr_gamma_control_manager_v1_destroy(app.gamma_control_manager);
+ wl_registry_destroy(registry);
+ wl_display_disconnect(display);
+ return EXIT_FAILURE;
+ }
+
+ struct output *output;
+ wl_list_for_each(output, &app.outputs, link) {
+ setup_gamma_control(&app, output);
+ }
+
+ if (wl_display_roundtrip(display) == -1 || wl_display_roundtrip(display) == -1) {
+ fprintf(stderr, "failed to apply the requested temperature\n");
+ zwlr_gamma_control_manager_v1_destroy(app.gamma_control_manager);
+ wl_registry_destroy(registry);
+ destroy_outputs(&app);
+ wl_display_disconnect(display);
+ return EXIT_FAILURE;
+ }
+
+ if (!any_output_applied(&app)) {
+ fprintf(stderr, "failed to apply %d K to any output\n", app.temperature);
+ zwlr_gamma_control_manager_v1_destroy(app.gamma_control_manager);
+ wl_registry_destroy(registry);
+ destroy_outputs(&app);
+ wl_display_disconnect(display);
+ return EXIT_FAILURE;
+ }
+
+ fprintf(stderr, "holding gamma controls open; terminate wsct to restore the original ramps\n");
+ while (wl_display_dispatch(display) != -1) {
+ }
+
+ zwlr_gamma_control_manager_v1_destroy(app.gamma_control_manager);
+ wl_registry_destroy(registry);
+ destroy_outputs(&app);
+ wl_display_disconnect(display);
+ return EXIT_SUCCESS;
+}
diff --git a/meson.build b/meson.build
new file mode 100644
index 0000000..e31dc2a
--- /dev/null
+++ b/meson.build
@@ -0,0 +1,79 @@
+project(
+ 'wsct',
+ 'c',
+ version: '0.4.0',
+ license: 'MIT',
+ meson_version: '>=0.56.0',
+ default_options: [
+ 'c_std=c11',
+ 'warning_level=3',
+ 'werror=true',
+ ],
+)
+
+add_project_arguments(
+ [
+ '-Wundef',
+ '-Wunused',
+ '-Wlogical-op',
+ '-Wmissing-include-dirs',
+ '-Wold-style-definition', # nop
+ '-Wpointer-arith',
+ '-Wstrict-prototypes',
+ '-Wmissing-prototypes',
+ '-Wno-implicit-fallthrough',
+ '-Wno-unknown-warning-option',
+ '-Wno-unused-command-line-argument',
+ '-Wvla',
+ '-Wl,--exclude-libs=ALL',
+ '-DWLSUNSET_VERSION="@0@"'.format(meson.project_version()),
+ ],
+ language: 'c',
+)
+
+scanner = find_program('wayland-scanner')
+scanner_private_code = generator(scanner, output: '@BASENAME@-protocol.c', arguments: ['private-code', '@INPUT@', '@OUTPUT@'])
+scanner_client_header = generator(scanner, output: '@BASENAME@-client-protocol.h', arguments: ['client-header', '@INPUT@', '@OUTPUT@'])
+
+protocols_src = [scanner_private_code.process('wlr-gamma-control-unstable-v1.xml')]
+protocols_headers = [scanner_client_header.process('wlr-gamma-control-unstable-v1.xml')]
+
+wl_client = dependency('wayland-client')
+wl_protocols = dependency('wayland-protocols')
+lib_protocols = static_library('protocols', protocols_src + protocols_headers, dependencies: wl_client)
+protocols_dep = declare_dependency(link_with: lib_protocols, sources: protocols_headers)
+
+cc = meson.get_compiler('c')
+m = cc.find_library('m')
+rt = cc.find_library('rt')
+
+executable(
+ 'wsct',
+ ['main.c', 'color.c'],
+ dependencies: [wl_client, protocols_dep, m, rt],
+ install: true,
+)
+
+scdoc = dependency('scdoc', required: get_option('man-pages'), version: '>= 1.9.7', native: true)
+
+if scdoc.found()
+ scdoc_prog = find_program(scdoc.get_variable(pkgconfig: 'scdoc'), native: true)
+ mandir = get_option('mandir')
+
+ foreach src : ['wsct.1.scd']
+ topic = src.split('.')[0]
+ section = src.split('.')[1]
+ output = '@0@.@1@'.format(topic, section)
+
+ custom_target(
+ output,
+ input: src,
+ output: output,
+ command: [
+ 'sh', '-c', '@0@ < @INPUT@ > @1@'.format(scdoc_prog.full_path(), output)
+ ],
+ install: true,
+ install_dir: '@0@/man@1@'.format(mandir, section)
+ )
+ endforeach
+endif
diff --git a/meson_options.txt b/meson_options.txt
new file mode 100644
index 0000000..e40a23d
--- /dev/null
+++ b/meson_options.txt
@@ -0,0 +1 @@
+option('man-pages', type: 'feature', value: 'auto', description: 'Generate and install man pages')
diff --git a/str_vec.c b/str_vec.c
new file mode 100644
index 0000000..82b61a4
--- /dev/null
+++ b/str_vec.c
@@ -0,0 +1,35 @@
+#define _XOPEN_SOURCE 700
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "str_vec.h"
+
+void str_vec_init(struct str_vec *vec) {
+ vec->data = NULL;
+ vec->len = 0;
+}
+
+void str_vec_push(struct str_vec *vec, const char *new_str) {
+ ++vec->len;
+ vec->data = realloc(vec->data, vec->len * sizeof(char*));
+ if (!vec->data) {
+ perror("str_vec_push failed to allocate memory for str_vec");
+ exit(EXIT_FAILURE);
+ }
+ vec->data[vec->len - 1] = strdup(new_str);
+}
+
+void str_vec_free(struct str_vec *vec) {
+ if (vec == NULL) {
+ return;
+ }
+ for (size_t i = 0; i < vec->len; ++i) {
+ if (vec->data[i] != NULL) {
+ free(vec->data[i]);
+ }
+ }
+ free(vec->data);
+ vec->data = NULL;
+ vec->len = 0;
+}
diff --git a/str_vec.h b/str_vec.h
new file mode 100644
index 0000000..f361b39
--- /dev/null
+++ b/str_vec.h
@@ -0,0 +1,15 @@
+#ifndef STR_VEC_H
+#define STR_VEC_H
+
+#include <stddef.h>
+
+struct str_vec {
+ char **data;
+ size_t len;
+};
+
+void str_vec_init(struct str_vec *vec);
+void str_vec_push(struct str_vec *vec, const char *new_str);
+void str_vec_free(struct str_vec *vec);
+
+#endif //STR_VEC_H
diff --git a/wlr-gamma-control-unstable-v1.xml b/wlr-gamma-control-unstable-v1.xml
new file mode 100644
index 0000000..16e0be8
--- /dev/null
+++ b/wlr-gamma-control-unstable-v1.xml
@@ -0,0 +1,126 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<protocol name="wlr_gamma_control_unstable_v1">
+ <copyright>
+ Copyright © 2015 Giulio camuffo
+ Copyright © 2018 Simon Ser
+
+ Permission to use, copy, modify, distribute, and sell this
+ software and its documentation for any purpose is hereby granted
+ without fee, provided that the above copyright notice appear in
+ all copies and that both that copyright notice and this permission
+ notice appear in supporting documentation, and that the name of
+ the copyright holders not be used in advertising or publicity
+ pertaining to distribution of the software without specific,
+ written prior permission. The copyright holders make no
+ representations about the suitability of this software for any
+ purpose. It is provided "as is" without express or implied
+ warranty.
+
+ THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
+ SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ SPECIAL, 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.
+ </copyright>
+
+ <description summary="manage gamma tables of outputs">
+ This protocol allows a privileged client to set the gamma tables for
+ outputs.
+
+ Warning! The protocol described in this file is experimental and
+ backward incompatible changes may be made. Backward compatible changes
+ may be added together with the corresponding interface version bump.
+ Backward incompatible changes are done by bumping the version number in
+ the protocol and interface names and resetting the interface version.
+ Once the protocol is to be declared stable, the 'z' prefix and the
+ version number in the protocol and interface names are removed and the
+ interface version number is reset.
+ </description>
+
+ <interface name="zwlr_gamma_control_manager_v1" version="1">
+ <description summary="manager to create per-output gamma controls">
+ This interface is a manager that allows creating per-output gamma
+ controls.
+ </description>
+
+ <request name="get_gamma_control">
+ <description summary="get a gamma control for an output">
+ Create a gamma control that can be used to adjust gamma tables for the
+ provided output.
+ </description>
+ <arg name="id" type="new_id" interface="zwlr_gamma_control_v1"/>
+ <arg name="output" type="object" interface="wl_output"/>
+ </request>
+
+ <request name="destroy" type="destructor">
+ <description summary="destroy the manager">
+ All objects created by the manager will still remain valid, until their
+ appropriate destroy request has been called.
+ </description>
+ </request>
+ </interface>
+
+ <interface name="zwlr_gamma_control_v1" version="1">
+ <description summary="adjust gamma tables for an output">
+ This interface allows a client to adjust gamma tables for a particular
+ output.
+
+ The client will receive the gamma size, and will then be able to set gamma
+ tables. At any time the compositor can send a failed event indicating that
+ this object is no longer valid.
+
+ There can only be at most one gamma control object per output, which
+ has exclusive access to this particular output. When the gamma control
+ object is destroyed, the gamma table is restored to its original value.
+ </description>
+
+ <event name="gamma_size">
+ <description summary="size of gamma ramps">
+ Advertise the size of each gamma ramp.
+
+ This event is sent immediately when the gamma control object is created.
+ </description>
+ <arg name="size" type="uint" summary="number of elements in a ramp"/>
+ </event>
+
+ <enum name="error">
+ <entry name="invalid_gamma" value="1" summary="invalid gamma tables"/>
+ </enum>
+
+ <request name="set_gamma">
+ <description summary="set the gamma table">
+ Set the gamma table. The file descriptor can be memory-mapped to provide
+ the raw gamma table, which contains successive gamma ramps for the red,
+ green and blue channels. Each gamma ramp is an array of 16-byte unsigned
+ integers which has the same length as the gamma size.
+
+ The file descriptor data must have the same length as three times the
+ gamma size.
+ </description>
+ <arg name="fd" type="fd" summary="gamma table file descriptor"/>
+ </request>
+
+ <event name="failed">
+ <description summary="object no longer valid">
+ This event indicates that the gamma control is no longer valid. This
+ can happen for a number of reasons, including:
+ - The output doesn't support gamma tables
+ - Setting the gamma tables failed
+ - Another client already has exclusive gamma control for this output
+ - The compositor has transferred gamma control to another client
+
+ Upon receiving this event, the client should destroy this object.
+ </description>
+ </event>
+
+ <request name="destroy" type="destructor">
+ <description summary="destroy this control">
+ Destroys the gamma control object. If the object is still valid, this
+ restores the original gamma tables.
+ </description>
+ </request>
+ </interface>
+</protocol>
diff --git a/wsct.1.scd b/wsct.1.scd
new file mode 100644
index 0000000..a12fda0
--- /dev/null
+++ b/wsct.1.scd
@@ -0,0 +1,41 @@
+wsct(1)
+
+# NAME
+
+wsct - set a fixed color temperature on Wayland outputs
+
+# SYNOPSIS
+
+*wsct* <temperature>
+
+# DESCRIPTION
+
+*wsct* applies a single color temperature to all available Wayland outputs
+using `wlr-gamma-control-unstable-v1`.
+
+The process stays running after the initial set. This is required by the
+protocol: when the gamma control object is destroyed, the compositor restores
+the original gamma ramps.
+
+# OPTIONS
+
+*-h*
+ Show the help message.
+
+*-v*
+ Show the version number.
+
+# EXAMPLE
+
+```
+wsct 4500
+```
+
+# NOTES
+
+The temperature argument is an integer in kelvin.
+
+# AUTHORS
+
+Derived from wlsunset, originally maintained by Kenny Levinsen
+<contact@kl.wtf>.