pax_global_header00006660000000000000000000000064144776356170014536gustar00rootroot0000000000000052 comment=879b39c175274fad020c2775e706c84a821b7040 wlsunset-0.3.0/000077500000000000000000000000001447763561700134225ustar00rootroot00000000000000wlsunset-0.3.0/.builds/000077500000000000000000000000001447763561700147625ustar00rootroot00000000000000wlsunset-0.3.0/.builds/alpine.yml000066400000000000000000000003331447763561700167540ustar00rootroot00000000000000image: alpine/edge packages: - meson - wayland - wayland-dev - wayland-protocols sources: - https://git.sr.ht/~kennylevinsen/wlsunset tasks: - build: | meson build wlsunset ninja -C build wlsunset-0.3.0/.builds/archlinux.yml000066400000000000000000000003101447763561700174740ustar00rootroot00000000000000image: archlinux packages: - meson - wayland - wayland-protocols sources: - https://git.sr.ht/~kennylevinsen/wlsunset tasks: - build: | meson build wlsunset ninja -C build wlsunset-0.3.0/.builds/freebsd.yml000066400000000000000000000003311447763561700171140ustar00rootroot00000000000000image: freebsd/latest packages: - meson - wayland - wayland-protocols - pkgconf sources: - https://git.sr.ht/~kennylevinsen/wlsunset tasks: - build: | meson build wlsunset ninja -C build wlsunset-0.3.0/.editorconfig000066400000000000000000000002011447763561700160700ustar00rootroot00000000000000root = true [*] end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true indent_style = tab indent_size = 8 wlsunset-0.3.0/.gitignore000066400000000000000000000000171447763561700154100ustar00rootroot00000000000000build/ .cache/ wlsunset-0.3.0/LICENSE000066400000000000000000000020361447763561700144300ustar00rootroot00000000000000Copyright 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. wlsunset-0.3.0/README.md000066400000000000000000000011631447763561700147020ustar00rootroot00000000000000# wlsunset Day/night gamma adjustments for Wayland compositors supporting `wlr-gamma-control-unstable-v1` & `xdg-output-unstable-v1`. # How to build and install ``` meson build ninja -C build sudo ninja -C build install ``` # How to use See the helptext (`wlsunset -h`) ## Example ``` # Beijing lat/long. wlsunset -l 39.9 -L 116.3 ``` Greater precision than one decimal place [serves no purpose](https://xkcd.com/2170/) other than padding the command-line. # Help Go to #kennylevinsen @ irc.libera.chat to discuss, or use [~kennylevinsen/wlsunset-devel@lists.sr.ht](https://lists.sr.ht/~kennylevinsen/wlsunset-devel) wlsunset-0.3.0/color_math.c000066400000000000000000000140731447763561700157220ustar00rootroot00000000000000#define _USE_MATH_DEFINES #define _XOPEN_SOURCE 700 #define _POSIX_C_SOURCE 200809L #include #include #include #include "color_math.h" static double SOLAR_START_TWILIGHT = RADIANS(90.833 + 6.0); static double SOLAR_END_TWILIGHT = RADIANS(90.833 - 3.0); static int days_in_year(int year) { int leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0; return leap ? 366 : 365; } static double date_orbit_angle(struct tm *tm) { return 2 * M_PI / (double)days_in_year(tm->tm_year + 1900) * tm->tm_yday; } static double equation_of_time(double orbit_angle) { // https://www.esrl.noaa.gov/gmd/grad/solcalc/solareqns.PDF return 4 * (0.000075 + 0.001868 * cos(orbit_angle) - 0.032077 * sin(orbit_angle) - 0.014615 * cos(2*orbit_angle) - 0.040849 * sin(2*orbit_angle)); } static double sun_declination(double orbit_angle) { // https://www.esrl.noaa.gov/gmd/grad/solcalc/solareqns.PDF return 0.006918 - 0.399912 * cos(orbit_angle) + 0.070257 * sin(orbit_angle) - 0.006758 * cos(2*orbit_angle) + 0.000907 * sin(2*orbit_angle) - 0.002697 * cos(3*orbit_angle) + 0.00148 * sin(3*orbit_angle); } static double sun_hour_angle(double latitude, double declination, double target_sun) { // https://www.esrl.noaa.gov/gmd/grad/solcalc/solareqns.PDF return acos(cos(target_sun) / cos(latitude) * cos(declination) - tan(latitude) * tan(declination)); } static time_t hour_angle_to_time(double hour_angle, double eqtime) { // https://www.esrl.noaa.gov/gmd/grad/solcalc/solareqns.PDF return DEGREES((4.0 * M_PI - 4 * hour_angle - eqtime) * 60); } static enum sun_condition condition(double latitude_rad, double sun_declination) { int sign_lat = signbit(latitude_rad) == 0; int sign_decl = signbit(sun_declination) == 0; return sign_lat == sign_decl ? MIDNIGHT_SUN : POLAR_NIGHT; } enum sun_condition calc_sun(struct tm *tm, double latitude, struct sun *sun) { double orbit_angle = date_orbit_angle(tm); double decl = sun_declination(orbit_angle); double eqtime = equation_of_time(orbit_angle); double ha_twilight = sun_hour_angle(latitude, decl, SOLAR_START_TWILIGHT); double ha_daylight = sun_hour_angle(latitude, decl, SOLAR_END_TWILIGHT); sun->dawn = hour_angle_to_time(fabs(ha_twilight), eqtime); sun->dusk = hour_angle_to_time(-fabs(ha_twilight), eqtime); sun->sunrise = hour_angle_to_time(fabs(ha_daylight), eqtime); sun->sunset = hour_angle_to_time(-fabs(ha_daylight), eqtime); return isnan(ha_twilight) || isnan(ha_daylight) ? condition(latitude, decl) : NORMAL; } /* * Illuminant D, or daylight locus, is is a "standard illuminant" used to * describe natural daylight. It is on this locus that D65, the whitepoint used * by most monitors and assumed by wlsunset, 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. This is not entirely equivalent to daylight due to * atmospheric effects. * * 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 srgb_gamma(double value, double gamma) { // https://en.wikipedia.org/wiki/SRGB if (value <= 0.0031308) { return 12.92 * value; } else { return pow(1.055 * value, 1.0/gamma) - 0.055; } } 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_srgb(const struct xyz *xyz) { // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html return (struct rgb) { .r = srgb_gamma(clamp(3.2404542 * xyz->x - 1.5371385 * xyz->y - 0.4985314 * xyz->z), 2.2), .g = srgb_gamma(clamp(-0.9692660 * xyz->x + 1.8760108 * xyz->y + 0.0415560 * xyz->z), 2.2), .b = srgb_gamma(clamp(0.0556434 * xyz->x - 0.2040259 * xyz->y + 1.0572252 * xyz->z), 2.2) }; } static void srgb_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}; } 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_srgb(&wp); srgb_normalize(&wp_rgb); return wp_rgb; } wlsunset-0.3.0/color_math.h000066400000000000000000000011061447763561700157200ustar00rootroot00000000000000#ifndef _COLOR_MATH_H #define _COLOR_MATH_H #include "math.h" #include "time.h" // These are macros so they can be applied to constants #define DEGREES(rad) ((rad) * 180.0 / M_PI) #define RADIANS(deg) ((deg) * M_PI / 180.0) enum sun_condition { NORMAL, MIDNIGHT_SUN, POLAR_NIGHT, SUN_CONDITION_LAST }; struct sun { time_t dawn; time_t sunrise; time_t sunset; time_t dusk; }; struct rgb { double r, g, b; }; struct xyz { double x, y, z; }; enum sun_condition calc_sun(struct tm *tm, double latitude, struct sun *sun); struct rgb calc_whitepoint(int temp); #endif wlsunset-0.3.0/main.c000066400000000000000000000575141447763561700145260ustar00rootroot00000000000000#define _DEFAULT_SOURCE #define _XOPEN_SOURCE 700 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "xdg-output-unstable-v1-client-protocol.h" #include "wlr-gamma-control-unstable-v1-client-protocol.h" #include "color_math.h" #include "str_vec.h" #if defined(SPEEDRUN) static time_t start = 0, offset = 0, multiplier = 1000; static void init_time(void) { tzset(); struct timespec realtime; clock_gettime(CLOCK_REALTIME, &realtime); offset = realtime.tv_sec; char *startstr = getenv("SPEEDRUN_START"); if (startstr != NULL) { start = atol(startstr); } else { start = offset; } char *multistr = getenv("SPEEDRUN_MULTIPLIER"); if (multistr != NULL) { multiplier = atol(multistr); } } static time_t get_time_sec(void) { struct timespec realtime; clock_gettime(CLOCK_REALTIME, &realtime); time_t now = start + ((realtime.tv_sec - offset) * multiplier + realtime.tv_nsec / (1000000000 / multiplier)); struct tm tm; localtime_r(&now, &tm); fprintf(stderr, "time in termina: %02d:%02d:%02d, %d/%d/%d\n", tm.tm_hour, tm.tm_min, tm.tm_sec, tm.tm_mday, tm.tm_mon+1, tm.tm_year + 1900); return now; } static void adjust_timerspec(struct itimerspec *timerspec) { int diff = timerspec->it_value.tv_sec - offset; timerspec->it_value.tv_sec = offset + diff / multiplier; timerspec->it_value.tv_nsec = (diff % multiplier) * (1000000000 / multiplier); } #else static inline void init_time(void) { tzset(); } static inline time_t get_time_sec(void) { struct timespec realtime; clock_gettime(CLOCK_REALTIME, &realtime); return realtime.tv_sec; } static inline void adjust_timerspec(struct itimerspec *timerspec) { (void)timerspec; } #endif static time_t get_timezone(void) { struct tm tm; time_t now = time(NULL); localtime_r(&now, &tm); return tm.tm_gmtoff; } static time_t round_day_offset(time_t now, time_t offset) { return now - ((now - offset) % 86400); } static time_t tomorrow(time_t now, time_t offset) { return round_day_offset(now, offset) + 86400; } static time_t longitude_time_offset(double longitude) { return -longitude * 43200 / M_PI; } static int max(int a, int b) { return a > b ? a : b; } struct config { int high_temp; int low_temp; double gamma; double longitude; double latitude; bool manual_time; time_t sunrise; time_t sunset; time_t duration; struct str_vec output_names; }; enum state { STATE_INITIAL, STATE_NORMAL, STATE_TRANSITION, STATE_STATIC, }; struct context { struct config config; struct sun sun; time_t longitude_time_offset; enum state state; enum sun_condition condition; time_t dawn_step_time; time_t dusk_step_time; time_t calc_day; bool new_output; struct wl_list outputs; timer_t timer; struct zwlr_gamma_control_manager_v1 *gamma_control_manager; struct zxdg_output_manager_v1 *xdg_output_manager; }; struct output { struct wl_list link; struct context *context; struct wl_output *wl_output; struct zxdg_output_v1 *xdg_output; struct zwlr_gamma_control_v1 *gamma_control; int table_fd; uint32_t id; uint32_t ramp_size; uint16_t *table; bool enabled; char *name; }; static void print_trajectory(struct context *ctx) { fprintf(stderr, "calculated sun trajectory: "); struct tm dawn, sunrise, sunset, dusk; switch (ctx->condition) { case NORMAL: localtime_r(&ctx->sun.dawn, &dawn); localtime_r(&ctx->sun.sunrise, &sunrise); localtime_r(&ctx->sun.sunset, &sunset); localtime_r(&ctx->sun.dusk, &dusk); fprintf(stderr, "dawn %02d:%02d, sunrise %02d:%02d, sunset %02d:%02d, dusk %02d:%02d\n", dawn.tm_hour, dawn.tm_min, sunrise.tm_hour, sunrise.tm_min, sunset.tm_hour, sunset.tm_min, dusk.tm_hour, dusk.tm_min); break; case MIDNIGHT_SUN: fprintf(stderr, "midnight sun\n"); return; case POLAR_NIGHT: fprintf(stderr, "polar night\n"); return; default: abort(); } } static int anim_kelvin_step = 25; static void recalc_stops(struct context *ctx, time_t now) { time_t day = round_day_offset(now, ctx->longitude_time_offset); if (day == ctx->calc_day) { return; } time_t last_day = ctx->calc_day; ctx->calc_day = day; enum sun_condition cond = NORMAL; if (ctx->config.manual_time) { ctx->state = STATE_NORMAL; ctx->sun.dawn = ctx->config.sunrise - ctx->config.duration + day; ctx->sun.sunrise = ctx->config.sunrise + day; ctx->sun.sunset = ctx->config.sunset + day; ctx->sun.dusk = ctx->config.sunset + ctx->config.duration + day; goto done; } struct sun sun; struct tm tm = { 0 }; gmtime_r(&day, &tm); cond = calc_sun(&tm, ctx->config.latitude, &sun); switch (cond) { case NORMAL: ctx->state = STATE_NORMAL; ctx->sun.dawn = sun.dawn + day; ctx->sun.sunrise = sun.sunrise + day; ctx->sun.sunset = sun.sunset + day; ctx->sun.dusk = sun.dusk + day; if (ctx->condition == MIDNIGHT_SUN) { // Yesterday had no sunset, so remove our sunrise. ctx->sun.dawn = day; ctx->sun.sunrise = day; } break; case MIDNIGHT_SUN: if (ctx->condition == POLAR_NIGHT) { fprintf(stderr, "warning: direct polar night to midnight sun transition\n"); } if (ctx->state != STATE_NORMAL) { ctx->state = STATE_STATIC; break; } // Borrow yesterday's sunrise to animate into the midnight sun sun.dawn = ctx->sun.dawn - last_day + day; sun.sunrise = ctx->sun.sunrise - last_day + day; ctx->state = STATE_TRANSITION; break; case POLAR_NIGHT: if (ctx->condition == MIDNIGHT_SUN) { fprintf(stderr, "warning: direct midnight sun to polar night transition\n"); } ctx->state = STATE_STATIC; break; default: abort(); } done: ctx->condition = cond; int temp_diff = ctx->config.high_temp - ctx->config.low_temp; ctx->dawn_step_time = max(1, (ctx->sun.sunrise - ctx->sun.dawn) * anim_kelvin_step / temp_diff); ctx->dusk_step_time = max(1, (ctx->sun.dusk - ctx->sun.sunset) * anim_kelvin_step / temp_diff); print_trajectory(ctx); } static int interpolate_temperature(time_t now, time_t start, time_t stop, int temp_start, int temp_stop) { if (start == stop) { return stop; } double time_pos = (double)(now - start) / (double)(stop - start); if (time_pos > 1.0) { time_pos = 1.0; } else if (time_pos < 0.0) { time_pos = 0.0; } int temp_pos = (double)(temp_stop - temp_start) * time_pos; return temp_start + temp_pos; } static int get_temperature_normal(const struct context *ctx, time_t now) { if (now < ctx->sun.dawn) { return ctx->config.low_temp; } else if (now < ctx->sun.sunrise) { return interpolate_temperature(now, ctx->sun.dawn, ctx->sun.sunrise, ctx->config.low_temp, ctx->config.high_temp); } else if (now < ctx->sun.sunset) { return ctx->config.high_temp; } else if (now < ctx->sun.dusk) { return interpolate_temperature(now, ctx->sun.sunset, ctx->sun.dusk, ctx->config.high_temp, ctx->config.low_temp); } else { return ctx->config.low_temp; } } static int get_temperature_transition(const struct context *ctx, time_t now) { switch (ctx->condition) { case MIDNIGHT_SUN: if (now < ctx->sun.sunrise) { return get_temperature_normal(ctx, now); } return ctx->config.high_temp; default: abort(); } } static int get_temperature(const struct context *ctx, time_t now) { switch (ctx->state) { case STATE_NORMAL: return get_temperature_normal(ctx, now); case STATE_TRANSITION: return get_temperature_transition(ctx, now); case STATE_STATIC: return ctx->condition == MIDNIGHT_SUN ? ctx->config.high_temp : ctx->config.low_temp; default: abort(); } } static time_t get_deadline_normal(const struct context *ctx, time_t now) { if (now < ctx->sun.dawn) { return ctx->sun.dawn; } else if (now < ctx->sun.sunrise) { return now + ctx->dawn_step_time; } else if (now < ctx->sun.sunset) { return ctx->sun.sunset; } else if (now < ctx->sun.dusk) { return now + ctx->dusk_step_time; } else { return tomorrow(now, ctx->longitude_time_offset); } } static time_t get_deadline_transition(const struct context *ctx, time_t now) { switch (ctx->condition) { case MIDNIGHT_SUN: if (now < ctx->sun.sunrise) { return get_deadline_normal(ctx, now); } // fallthrough case POLAR_NIGHT: return tomorrow(now, ctx->longitude_time_offset); default: abort(); } } static void update_timer(const struct context *ctx, timer_t timer, time_t now) { time_t deadline; switch (ctx->state) { case STATE_NORMAL: deadline = get_deadline_normal(ctx, now); break; case STATE_TRANSITION: deadline = get_deadline_transition(ctx, now); break; case STATE_STATIC: deadline = tomorrow(now, ctx->longitude_time_offset); break; default: abort(); } assert(deadline > now); struct itimerspec timerspec = { .it_interval = {0}, .it_value = { .tv_sec = deadline, .tv_nsec = 0, } }; adjust_timerspec(&timerspec); timer_settime(timer, TIMER_ABSTIME, &timerspec, NULL); } static int create_anonymous_file(off_t size) { char template[] = "/tmp/wlsunset-shared-XXXXXX"; int fd = mkstemp(template); if (fd < 0) { return -1; } int ret; do { errno = 0; ret = ftruncate(fd, size); } while (errno == EINTR); if (ret < 0) { close(fd); return -1; } unlink(template); return fd; } static int create_gamma_table(uint32_t ramp_size, uint16_t **table) { size_t table_size = ramp_size * 3 * sizeof(uint16_t); int fd = create_anonymous_file(table_size); if (fd < 0) { fprintf(stderr, "failed to create anonymous file\n"); return -1; } void *data = mmap(NULL, table_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (data == MAP_FAILED) { fprintf(stderr, "failed to mmap()\n"); close(fd); return -1; } *table = data; return fd; } 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; output->ramp_size = ramp_size; if (output->table_fd != -1) { close(output->table_fd); } output->table_fd = create_gamma_table(ramp_size, &output->table); output->context->new_output = true; if (output->table_fd < 0) { fprintf(stderr, "could not create gamma table for output %d\n", output->id); 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 of output %d failed\n", output->id); zwlr_gamma_control_v1_destroy(output->gamma_control); output->gamma_control = NULL; if (output->table_fd != -1) { close(output->table_fd); output->table_fd = -1; } } 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 xdg_output_handle_logical_position(void *data, struct zxdg_output_v1 *xdg_output, int32_t x, int32_t y) { (void)data, (void)xdg_output, (void)x, (void)y; } static void xdg_output_handle_logical_size(void *data, struct zxdg_output_v1 *xdg_output, int32_t width, int32_t height) { (void)data, (void)xdg_output, (void)width, (void)height; } static void xdg_output_handle_done(void *data, struct zxdg_output_v1 *xdg_output) { (void)data, (void)xdg_output; } static void xdg_output_handle_name(void *data, struct zxdg_output_v1 *xdg_output, const char *name) { (void)xdg_output; struct output *output = data; output->name = strdup(name); struct config *cfg = &output->context->config; for (size_t idx = 0; idx < cfg->output_names.len; ++idx) { if (strcmp(output->name, cfg->output_names.data[idx]) == 0) { fprintf(stderr, "enabling output %s by name\n", output->name); output->enabled = true; return; } } } static void xdg_output_handle_description(void *data, struct zxdg_output_v1 *xdg_output, const char *description) { (void)data, (void)xdg_output, (void)description; } static const struct zxdg_output_v1_listener xdg_output_listener = { .logical_position = xdg_output_handle_logical_position, .logical_size = xdg_output_handle_logical_size, .done = xdg_output_handle_done, .name = xdg_output_handle_name, .description = xdg_output_handle_description, }; static void setup_xdg_output(struct context *ctx, struct output *output) { if (output->xdg_output != NULL) { return; } if (ctx->xdg_output_manager == NULL) { fprintf(stderr, "skipping setup of output %d: xdg_output_manager is missing\n", output->id); return; } output->xdg_output = zxdg_output_manager_v1_get_xdg_output( ctx->xdg_output_manager, output->wl_output); zxdg_output_v1_add_listener(output->xdg_output, &xdg_output_listener, output); } static void setup_gamma_control(struct context *ctx, struct output *output) { if (output->gamma_control != NULL) { return; } if (ctx->gamma_control_manager == NULL) { fprintf(stderr, "skipping setup of output %d: gamma_control_manager missing\n", output->id); return; } output->gamma_control = zwlr_gamma_control_manager_v1_get_gamma_control( ctx->gamma_control_manager, output->wl_output); zwlr_gamma_control_v1_add_listener(output->gamma_control, &gamma_control_listener, output); } static void registry_handle_global(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) { (void)version; struct context *ctx = (struct context *)data; if (strcmp(interface, wl_output_interface.name) == 0) { fprintf(stderr, "registry: adding output %d\n", name); struct output *output = calloc(1, sizeof(struct output)); output->id = name; output->wl_output = wl_registry_bind(registry, name, &wl_output_interface, 1); output->table_fd = -1; output->enabled = true; output->context = ctx; wl_list_insert(&ctx->outputs, &output->link); if (ctx->config.output_names.len > 0) { setup_xdg_output(ctx, output); output->enabled = false; } setup_gamma_control(ctx, output); } else if (ctx->config.output_names.len > 0 && strcmp(interface, zxdg_output_manager_v1_interface.name) == 0) { ctx->xdg_output_manager = wl_registry_bind(registry, name, &zxdg_output_manager_v1_interface, version); } else if (strcmp(interface, zwlr_gamma_control_manager_v1_interface.name) == 0) { ctx->gamma_control_manager = wl_registry_bind(registry, name, &zwlr_gamma_control_manager_v1_interface, 1); } } static void registry_handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) { (void)registry; struct context *ctx = (struct context *)data; struct output *output, *tmp; wl_list_for_each_safe(output, tmp, &ctx->outputs, link) { if (output->id == name) { fprintf(stderr, "registry: removing output %d\n", name); wl_list_remove(&output->link); if (output->xdg_output != NULL) { zxdg_output_v1_destroy(output->xdg_output); } if (output->gamma_control != NULL) { zwlr_gamma_control_v1_destroy(output->gamma_control); } if (output->table_fd != -1) { close(output->table_fd); } free(output); break; } } } static const struct wl_registry_listener registry_listener = { .global = registry_handle_global, .global_remove = registry_handle_global_remove, }; 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 / (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 void output_set_whitepoint(struct output *output, struct rgb *wp, double gamma) { if (!output->enabled || output->gamma_control == NULL || output->table_fd == -1) { return; } fill_gamma_table(output->table, output->ramp_size, wp->r, wp->g, wp->b, gamma); lseek(output->table_fd, 0, SEEK_SET); zwlr_gamma_control_v1_set_gamma(output->gamma_control, output->table_fd); } static void set_temperature(struct wl_list *outputs, int temp, double gamma) { struct rgb wp = calc_whitepoint(temp); struct output *output; wl_list_for_each(output, outputs, link) { fprintf(stderr, "setting temperature on output '%d' to %d K\n", output->id, temp); output_set_whitepoint(output, &wp, gamma); } } static int timer_fired = 0; static int timer_signal_fds[2]; static int display_dispatch(struct wl_display *display, int timeout) { if (wl_display_prepare_read(display) == -1) { return wl_display_dispatch_pending(display); } struct pollfd pfd[2]; pfd[0].fd = wl_display_get_fd(display); pfd[1].fd = timer_signal_fds[0]; pfd[0].events = POLLOUT; // If we hit EPIPE we might have hit a protocol error. Continue reading // so that we can see what happened. while (wl_display_flush(display) == -1 && errno != EPIPE) { if (errno != EAGAIN) { wl_display_cancel_read(display); return -1; } // We only poll the wayland fd here while (poll(pfd, 1, timeout) == -1) { if (errno != EINTR) { wl_display_cancel_read(display); return -1; } } } pfd[0].events = POLLIN; pfd[1].events = POLLIN; while (poll(pfd, 2, timeout) == -1) { if (errno != EINTR) { wl_display_cancel_read(display); return -1; } } if (pfd[1].revents & POLLIN) { // Empty signal fd char garbage[8]; if (read(timer_signal_fds[0], &garbage, sizeof garbage) == -1 && errno != EAGAIN) { return -1; } } if ((pfd[0].revents & POLLIN) == 0) { wl_display_cancel_read(display); return 0; } if (wl_display_read_events(display) == -1) { return -1; } return wl_display_dispatch_pending(display); } static void timer_signal(int signal) { (void)signal; timer_fired = true; if (write(timer_signal_fds[1], "\0", 1) == -1 && errno != EAGAIN) { // This is unfortunate. } } static int set_nonblock(int fd) { int flags; if ((flags = fcntl(fd, F_GETFL)) == -1 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) { return -1; } return 0; } static int setup_timer(struct context *ctx) { struct sigaction timer_action = { .sa_handler = timer_signal, .sa_flags = 0, }; if (pipe(timer_signal_fds) == -1) { fprintf(stderr, "could not create signal pipe: %s\n", strerror(errno)); return -1; } if (set_nonblock(timer_signal_fds[0]) == -1 || set_nonblock(timer_signal_fds[1]) == -1) { fprintf(stderr, "could not set nonblock on signal pipe: %s\n", strerror(errno)); return -1; } if (sigaction(SIGALRM, &timer_action, NULL) == -1) { fprintf(stderr, "could not configure alarm handler: %s\n", strerror(errno)); return -1; } if (timer_create(CLOCK_REALTIME, NULL, &ctx->timer) == -1) { fprintf(stderr, "could not configure timer: %s\n", strerror(errno)); return -1; } return 0; } static int wlrun(struct config cfg) { // Initialize defaults struct context ctx = { .sun = { 0 }, .condition = SUN_CONDITION_LAST, .state = STATE_INITIAL, .config = cfg, }; if (!cfg.manual_time) { ctx.longitude_time_offset = longitude_time_offset(cfg.longitude); } else { ctx.longitude_time_offset = -get_timezone(); } wl_list_init(&ctx.outputs); if (setup_timer(&ctx) == -1) { return EXIT_FAILURE; } struct wl_display *display = wl_display_connect(NULL); if (display == NULL) { fprintf(stderr, "failed to create display\n"); return EXIT_FAILURE; } struct wl_registry *registry = wl_display_get_registry(display); wl_registry_add_listener(registry, ®istry_listener, &ctx); wl_display_roundtrip(display); if (ctx.config.output_names.len > 0 && ctx.xdg_output_manager == NULL) { fprintf(stderr, "compositor doesn't support xdg-output-unstable-v1\n"); return EXIT_FAILURE; } if (ctx.gamma_control_manager == NULL) { fprintf(stderr, "compositor doesn't support wlr-gamma-control-unstable-v1\n"); return EXIT_FAILURE; } struct output *output; wl_list_for_each(output, &ctx.outputs, link) { if (ctx.config.output_names.len > 0) { setup_xdg_output(&ctx, output); } setup_gamma_control(&ctx, output); } wl_display_roundtrip(display); time_t now = get_time_sec(); recalc_stops(&ctx, now); update_timer(&ctx, ctx.timer, now); int temp = get_temperature(&ctx, now); set_temperature(&ctx.outputs, temp, ctx.config.gamma); int old_temp = temp; while (display_dispatch(display, -1) != -1) { if (timer_fired) { timer_fired = false; now = get_time_sec(); recalc_stops(&ctx, now); update_timer(&ctx, ctx.timer, now); if ((temp = get_temperature(&ctx, now)) != old_temp) { old_temp = temp; ctx.new_output = false; set_temperature(&ctx.outputs, temp, ctx.config.gamma); } } else if (ctx.new_output) { ctx.new_output = false; set_temperature(&ctx.outputs, temp, ctx.config.gamma); } } return EXIT_SUCCESS; } static int parse_time_of_day(const char *s, time_t *time) { struct tm tm = { 0 }; if (strptime(s, "%H:%M", &tm) == NULL) { return -1; } *time = tm.tm_hour * 3600 + tm.tm_min * 60; return 0; } static const char usage[] = "usage: %s [options]\n" " -h show this help message\n" " -v show the version number\n" " -o name of output (display) to use,\n" " by default all outputs are used\n" " can be specified multiple times\n" " -t set low temperature (default: 4000)\n" " -T set high temperature (default: 6500)\n" " -l set latitude (e.g. 39.9)\n" " -L set longitude (e.g. 116.3)\n" " -S set manual sunrise (e.g. 06:30)\n" " -s set manual sunset (e.g. 18:30)\n" " -d set manual duration in seconds (e.g. 1800)\n" " -g set gamma (default: 1.0)\n"; int main(int argc, char *argv[]) { #ifdef SPEEDRUN fprintf(stderr, "warning: speedrun mode enabled\n"); #endif init_time(); struct config config = { .latitude = NAN, .longitude = NAN, .high_temp = 6500, .low_temp = 4000, .gamma = 1.0, }; str_vec_init(&config.output_names); int ret = EXIT_FAILURE; int opt; while ((opt = getopt(argc, argv, "hvo:t:T:l:L:S:s:d:g:")) != -1) { switch (opt) { case 'o': str_vec_push(&config.output_names, optarg); break; case 't': config.low_temp = strtol(optarg, NULL, 10); break; case 'T': config.high_temp = strtol(optarg, NULL, 10); break; case 'l': config.latitude = strtod(optarg, NULL); break; case 'L': config.longitude = strtod(optarg, NULL); break; case 'S': if (parse_time_of_day(optarg, &config.sunrise) != 0) { fprintf(stderr, "invalid time, expected HH:MM, got %s\n", optarg); goto end; } config.manual_time = true; break; case 's': if (parse_time_of_day(optarg, &config.sunset) != 0) { fprintf(stderr, "invalid time, expected HH:MM, got %s\n", optarg); goto end; } config.manual_time = true; break; case 'd': config.duration = strtol(optarg, NULL, 10); break; case 'g': config.gamma = strtod(optarg, NULL); break; case 'v': printf("wlsunset version %s\n", WLSUNSET_VERSION); ret = EXIT_SUCCESS; goto end; case 'h': ret = EXIT_SUCCESS; default: fprintf(stderr, usage, argv[0]); goto end; } } if (config.high_temp <= config.low_temp) { fprintf(stderr, "high temp (%d) must be higher than low (%d) temp\n", config.high_temp, config.low_temp); goto end; } if (config.manual_time) { if (!isnan(config.latitude) || !isnan(config.longitude)) { fprintf(stderr, "latitude and longitude are not valid in manual time mode\n"); goto end; } } else { if (config.latitude > 90.0 || config.latitude < -90.0) { fprintf(stderr, "latitude (%lf) must be in interval [-90,90]\n", config.latitude); goto end; } config.latitude = RADIANS(config.latitude); if (config.longitude > 180.0 || config.longitude < -180.0) { fprintf(stderr, "longitude (%lf) must be in interval [-180,180]\n", config.longitude); goto end; } config.longitude = RADIANS(config.longitude); } ret = wlrun(config); end: str_vec_free(&config.output_names); return ret; } wlsunset-0.3.0/meson.build000066400000000000000000000043261447763561700155710ustar00rootroot00000000000000project( 'wlsunset', 'c', version: '0.3.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', 'xdg-output-unstable-v1.xml')] protocols_headers = [scanner_client_header.process('wlr-gamma-control-unstable-v1.xml', 'xdg-output-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( 'wlsunset', ['main.c', 'color_math.c', 'str_vec.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 : ['wlsunset.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 wlsunset-0.3.0/meson_options.txt000066400000000000000000000001431447763561700170550ustar00rootroot00000000000000option('man-pages', type: 'feature', value: 'auto', description: 'Generate and install man pages') wlsunset-0.3.0/str_vec.c000066400000000000000000000011011447763561700152240ustar00rootroot00000000000000#define _XOPEN_SOURCE 700 #include #include #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*)); 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; } wlsunset-0.3.0/str_vec.h000066400000000000000000000004131447763561700152360ustar00rootroot00000000000000#ifndef STR_VEC_H #define STR_VEC_H #include 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 wlsunset-0.3.0/wlr-gamma-control-unstable-v1.xml000066400000000000000000000126031447763561700216470ustar00rootroot00000000000000 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. 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. This interface is a manager that allows creating per-output gamma controls. Create a gamma control that can be used to adjust gamma tables for the provided output. All objects created by the manager will still remain valid, until their appropriate destroy request has been called. 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. Advertise the size of each gamma ramp. This event is sent immediately when the gamma control object is created. 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. 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. Destroys the gamma control object. If the object is still valid, this restores the original gamma tables. wlsunset-0.3.0/wlsunset.1.scd000066400000000000000000000017561447763561700161510ustar00rootroot00000000000000wlsunset(1) # NAME wlsunet - Day/night gamma adjustments for Wayland compositors supporting wlr-gamma-control-unstable-v1 # SYNOPSIS *wlsunset* [options...] # OPTIONS *-h* show this help message *-T* set high temperature (default: 6500) *-t* set low temperature (default: 4000) *-l* set latitude (e.g. 39.9) *-L* set longitude (e.g. 116.3) *-S* Manual time for sunrise as HH:MM (e.g. 06:30) *-s* Manual time for sunset as HH:MM (e.g. 18:30) *-d* Manual animation time in seconds (e.g. 1800) Only applicable when using manual sunset/sunrise times. *-g* set gamma (default: 1.0) # EXAMPLE ``` # Beijing lat/long. wlsunset -l 39.9 -L 116.3 ``` Greater precision than one decimal place serves no purpose (https://xkcd.com/2170/) other than padding the command-line. # AUTHORS Maintained by Kenny Levinsen . For more information about wlsunset development, see https://sr.ht/~kennylevinsen/wlsunset. wlsunset-0.3.0/xdg-output-unstable-v1.xml000066400000000000000000000225121447763561700204250ustar00rootroot00000000000000 Copyright © 2017 Red Hat Inc. 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 (including the next paragraph) 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. This protocol aims at describing outputs in a way which is more in line with the concept of an output on desktop oriented systems. Some information are more specific to the concept of an output for a desktop oriented system and may not make sense in other applications, such as IVI systems for example. Typically, the global compositor space on a desktop system is made of a contiguous or overlapping set of rectangular regions. Some of the information provided in this protocol might be identical to their counterparts already available from wl_output, in which case the information provided by this protocol should be preferred to their equivalent in wl_output. The goal is to move the desktop specific concepts (such as output location within the global compositor space, the connector name and types, etc.) out of the core wl_output protocol. 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. A global factory interface for xdg_output objects. Using this request a client can tell the server that it is not going to use the xdg_output_manager object anymore. Any objects already created through this instance are not affected. This creates a new xdg_output object for the given wl_output. An xdg_output describes part of the compositor geometry. This typically corresponds to a monitor that displays part of the compositor space. For objects version 3 onwards, after all xdg_output properties have been sent (when the object is created and when properties are updated), a wl_output.done event is sent. This allows changes to the output properties to be seen as atomic, even if they happen via multiple events. Using this request a client can tell the server that it is not going to use the xdg_output object anymore. The position event describes the location of the wl_output within the global compositor space. The logical_position event is sent after creating an xdg_output (see xdg_output_manager.get_xdg_output) and whenever the location of the output changes within the global compositor space. The logical_size event describes the size of the output in the global compositor space. For example, a surface without any buffer scale, transformation nor rotation set, with the size matching the logical_size will have the same size as the corresponding output when displayed. Most regular Wayland clients should not pay attention to the logical size and would rather rely on xdg_shell interfaces. Some clients such as Xwayland, however, need this to configure their surfaces in the global compositor space as the compositor may apply a different scale from what is advertised by the output scaling property (to achieve fractional scaling, for example). For example, for a wl_output mode 3840×2160 and a scale factor 2: - A compositor not scaling the surface buffers will advertise a logical size of 3840×2160, - A compositor automatically scaling the surface buffers will advertise a logical size of 1920×1080, - A compositor using a fractional scale of 1.5 will advertise a logical size of 2560×1440. For example, for a wl_output mode 1920×1080 and a 90 degree rotation, the compositor will advertise a logical size of 1080x1920. The logical_size event is sent after creating an xdg_output (see xdg_output_manager.get_xdg_output) and whenever the logical size of the output changes, either as a result of a change in the applied scale or because of a change in the corresponding output mode(see wl_output.mode) or transform (see wl_output.transform). This event is sent after all other properties of an xdg_output have been sent. This allows changes to the xdg_output properties to be seen as atomic, even if they happen via multiple events. For objects version 3 onwards, this event is deprecated. Compositors are not required to send it anymore and must send wl_output.done instead. Many compositors will assign names to their outputs, show them to the user, allow them to be configured by name, etc. The client may wish to know this name as well to offer the user similar behaviors. The naming convention is compositor defined, but limited to alphanumeric characters and dashes (-). Each name is unique among all wl_output globals, but if a wl_output global is destroyed the same name may be reused later. The names will also remain consistent across sessions with the same hardware and software configuration. Examples of names include 'HDMI-A-1', 'WL-1', 'X11-1', etc. However, do not assume that the name is a reflection of an underlying DRM connector, X11 connection, etc. The name event is sent after creating an xdg_output (see xdg_output_manager.get_xdg_output). This event is only sent once per xdg_output, and the name does not change over the lifetime of the wl_output global. Many compositors can produce human-readable descriptions of their outputs. The client may wish to know this description as well, to communicate the user for various purposes. The description is a UTF-8 string with no convention defined for its contents. Examples might include 'Foocorp 11" Display' or 'Virtual X11 output via :1'. The description event is sent after creating an xdg_output (see xdg_output_manager.get_xdg_output) and whenever the description changes. The description is optional, and may not be sent at all. For objects of version 2 and lower, this event is only sent once per xdg_output, and the description does not change over the lifetime of the wl_output global.